From e4bb50b426bc9cadacf30f925866b033ccaf1202 Mon Sep 17 00:00:00 2001 From: Daniel Sainati Date: Tue, 23 Jan 2024 11:57:13 -0500 Subject: [PATCH 1/5] add v0.42 parser package under old_parser --- runtime/ast/access.go | 5 + runtime/common/declarationkind.go | 5 + runtime/common/declarationkind_string.go | 37 +- runtime/old_parser/benchmark_test.go | 230 + runtime/old_parser/comment.go | 64 + runtime/old_parser/declaration.go | 1578 ++++ runtime/old_parser/declaration_test.go | 7083 +++++++++++++++++ runtime/old_parser/docstring.go | 68 + runtime/old_parser/docstring_test.go | 68 + runtime/old_parser/errors.go | 256 + runtime/old_parser/expression.go | 1818 +++++ runtime/old_parser/expression_test.go | 6458 +++++++++++++++ runtime/old_parser/function.go | 401 + .../old_parser/invalidnumberliteralkind.go | 52 + .../invalidnumberliteralkind_string.go | 27 + runtime/old_parser/keyword.go | 71 + runtime/old_parser/lexer/lexer.go | 455 ++ runtime/old_parser/lexer/lexer_test.go | 2643 ++++++ runtime/old_parser/lexer/state.go | 342 + runtime/old_parser/lexer/token.go | 39 + runtime/old_parser/lexer/tokenstream.go | 29 + runtime/old_parser/lexer/tokentype.go | 222 + runtime/old_parser/parser.go | 693 ++ runtime/old_parser/parser_test.go | 1013 +++ runtime/old_parser/statement.go | 826 ++ runtime/old_parser/statement_test.go | 2500 ++++++ runtime/old_parser/transaction.go | 292 + runtime/old_parser/type.go | 988 +++ runtime/old_parser/type_test.go | 2839 +++++++ 29 files changed, 31084 insertions(+), 18 deletions(-) create mode 100644 runtime/old_parser/benchmark_test.go create mode 100644 runtime/old_parser/comment.go create mode 100644 runtime/old_parser/declaration.go create mode 100644 runtime/old_parser/declaration_test.go create mode 100644 runtime/old_parser/docstring.go create mode 100644 runtime/old_parser/docstring_test.go create mode 100644 runtime/old_parser/errors.go create mode 100644 runtime/old_parser/expression.go create mode 100644 runtime/old_parser/expression_test.go create mode 100644 runtime/old_parser/function.go create mode 100644 runtime/old_parser/invalidnumberliteralkind.go create mode 100644 runtime/old_parser/invalidnumberliteralkind_string.go create mode 100644 runtime/old_parser/keyword.go create mode 100644 runtime/old_parser/lexer/lexer.go create mode 100644 runtime/old_parser/lexer/lexer_test.go create mode 100644 runtime/old_parser/lexer/state.go create mode 100644 runtime/old_parser/lexer/token.go create mode 100644 runtime/old_parser/lexer/tokenstream.go create mode 100644 runtime/old_parser/lexer/tokentype.go create mode 100644 runtime/old_parser/parser.go create mode 100644 runtime/old_parser/parser_test.go create mode 100644 runtime/old_parser/statement.go create mode 100644 runtime/old_parser/statement_test.go create mode 100644 runtime/old_parser/transaction.go create mode 100644 runtime/old_parser/type.go create mode 100644 runtime/old_parser/type_test.go diff --git a/runtime/ast/access.go b/runtime/ast/access.go index 969aa58b14..123fda02fa 100644 --- a/runtime/ast/access.go +++ b/runtime/ast/access.go @@ -215,6 +215,7 @@ const ( AccessContract AccessAccount AccessAll + PubSettableLegacy ) func PrimitiveAccessCount() int { @@ -253,6 +254,8 @@ func (a PrimitiveAccess) Keyword() string { return "access(account)" case AccessContract: return "access(contract)" + case PubSettableLegacy: + return "pub(set)" } panic(errors.NewUnreachableError()) @@ -270,6 +273,8 @@ func (a PrimitiveAccess) Description() string { return "account" case AccessContract: return "contract" + case PubSettableLegacy: + return "legacy public settable" } panic(errors.NewUnreachableError()) diff --git a/runtime/common/declarationkind.go b/runtime/common/declarationkind.go index 70ca5f879b..ccdd061b3b 100644 --- a/runtime/common/declarationkind.go +++ b/runtime/common/declarationkind.go @@ -44,6 +44,7 @@ const ( DeclarationKindEvent DeclarationKindField DeclarationKindInitializer + DeclarationKindDestructorLegacy DeclarationKindStructureInterface DeclarationKindResourceInterface DeclarationKindContractInterface @@ -116,6 +117,8 @@ func (k DeclarationKind) Name() string { return "field" case DeclarationKindInitializer: return "initializer" + case DeclarationKindDestructorLegacy: + return "legacy destructor" case DeclarationKindAttachment: return "attachment" case DeclarationKindStructureInterface: @@ -173,6 +176,8 @@ func (k DeclarationKind) Keywords() string { return "event" case DeclarationKindInitializer: return "init" + case DeclarationKindDestructorLegacy: + return "destroy" case DeclarationKindAttachment: return "attachment" case DeclarationKindStructureInterface: diff --git a/runtime/common/declarationkind_string.go b/runtime/common/declarationkind_string.go index 4918be2db6..9a43cc44f5 100644 --- a/runtime/common/declarationkind_string.go +++ b/runtime/common/declarationkind_string.go @@ -22,27 +22,28 @@ func _() { _ = x[DeclarationKindEvent-11] _ = x[DeclarationKindField-12] _ = x[DeclarationKindInitializer-13] - _ = x[DeclarationKindStructureInterface-14] - _ = x[DeclarationKindResourceInterface-15] - _ = x[DeclarationKindContractInterface-16] - _ = x[DeclarationKindEntitlement-17] - _ = x[DeclarationKindEntitlementMapping-18] - _ = x[DeclarationKindImport-19] - _ = x[DeclarationKindSelf-20] - _ = x[DeclarationKindBase-21] - _ = x[DeclarationKindTransaction-22] - _ = x[DeclarationKindPrepare-23] - _ = x[DeclarationKindExecute-24] - _ = x[DeclarationKindTypeParameter-25] - _ = x[DeclarationKindPragma-26] - _ = x[DeclarationKindEnum-27] - _ = x[DeclarationKindEnumCase-28] - _ = x[DeclarationKindAttachment-29] + _ = x[DeclarationKindDestructorLegacy-14] + _ = x[DeclarationKindStructureInterface-15] + _ = x[DeclarationKindResourceInterface-16] + _ = x[DeclarationKindContractInterface-17] + _ = x[DeclarationKindEntitlement-18] + _ = x[DeclarationKindEntitlementMapping-19] + _ = x[DeclarationKindImport-20] + _ = x[DeclarationKindSelf-21] + _ = x[DeclarationKindBase-22] + _ = x[DeclarationKindTransaction-23] + _ = x[DeclarationKindPrepare-24] + _ = x[DeclarationKindExecute-25] + _ = x[DeclarationKindTypeParameter-26] + _ = x[DeclarationKindPragma-27] + _ = x[DeclarationKindEnum-28] + _ = x[DeclarationKindEnumCase-29] + _ = x[DeclarationKindAttachment-30] } -const _DeclarationKind_name = "DeclarationKindUnknownDeclarationKindValueDeclarationKindFunctionDeclarationKindVariableDeclarationKindConstantDeclarationKindTypeDeclarationKindParameterDeclarationKindArgumentLabelDeclarationKindStructureDeclarationKindResourceDeclarationKindContractDeclarationKindEventDeclarationKindFieldDeclarationKindInitializerDeclarationKindStructureInterfaceDeclarationKindResourceInterfaceDeclarationKindContractInterfaceDeclarationKindEntitlementDeclarationKindEntitlementMappingDeclarationKindImportDeclarationKindSelfDeclarationKindBaseDeclarationKindTransactionDeclarationKindPrepareDeclarationKindExecuteDeclarationKindTypeParameterDeclarationKindPragmaDeclarationKindEnumDeclarationKindEnumCaseDeclarationKindAttachment" +const _DeclarationKind_name = "DeclarationKindUnknownDeclarationKindValueDeclarationKindFunctionDeclarationKindVariableDeclarationKindConstantDeclarationKindTypeDeclarationKindParameterDeclarationKindArgumentLabelDeclarationKindStructureDeclarationKindResourceDeclarationKindContractDeclarationKindEventDeclarationKindFieldDeclarationKindInitializerDeclarationKindDestructorLegacyDeclarationKindStructureInterfaceDeclarationKindResourceInterfaceDeclarationKindContractInterfaceDeclarationKindEntitlementDeclarationKindEntitlementMappingDeclarationKindImportDeclarationKindSelfDeclarationKindBaseDeclarationKindTransactionDeclarationKindPrepareDeclarationKindExecuteDeclarationKindTypeParameterDeclarationKindPragmaDeclarationKindEnumDeclarationKindEnumCaseDeclarationKindAttachment" -var _DeclarationKind_index = [...]uint16{0, 22, 42, 65, 88, 111, 130, 154, 182, 206, 229, 252, 272, 292, 318, 351, 383, 415, 441, 474, 495, 514, 533, 559, 581, 603, 631, 652, 671, 694, 719} +var _DeclarationKind_index = [...]uint16{0, 22, 42, 65, 88, 111, 130, 154, 182, 206, 229, 252, 272, 292, 318, 349, 382, 414, 446, 472, 505, 526, 545, 564, 590, 612, 634, 662, 683, 702, 725, 750} func (i DeclarationKind) String() string { if i >= DeclarationKind(len(_DeclarationKind_index)-1) { diff --git a/runtime/old_parser/benchmark_test.go b/runtime/old_parser/benchmark_test.go new file mode 100644 index 0000000000..6abcfe4bb7 --- /dev/null +++ b/runtime/old_parser/benchmark_test.go @@ -0,0 +1,230 @@ +/* + * Cadence - The resource-oriented smart contract programming language + * + * Copyright Dapper Labs, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package old_parser + +import ( + "fmt" + "math" + "math/rand" + "strconv" + "strings" + "testing" + + "github.com/onflow/cadence/runtime/common" +) + +func BenchmarkParseDeploy(b *testing.B) { + + b.Run("byte array", func(b *testing.B) { + + var builder strings.Builder + for i := 0; i < 15000; i++ { + if i > 0 { + builder.WriteString(", ") + } + builder.WriteString(strconv.Itoa(rand.Intn(math.MaxUint8))) + } + + transaction := []byte(fmt.Sprintf(` + transaction { + execute { + AuthAccount(publicKeys: [], code: [%s]) + } + } + `, + builder.String(), + )) + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + _, err := ParseProgram(nil, transaction, Config{}) + if err != nil { + b.FailNow() + } + } + }) + + b.Run("decode hex", func(b *testing.B) { + + var builder strings.Builder + for i := 0; i < 15000; i++ { + builder.WriteString(fmt.Sprintf("%02x", i)) + } + + transaction := []byte(fmt.Sprintf(` + transaction { + execute { + AuthAccount(publicKeys: [], code: "%s".decodeHex()) + } + } + `, + builder.String(), + )) + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + _, err := ParseProgram(nil, transaction, Config{}) + if err != nil { + b.FailNow() + } + } + }) +} + +const fungibleTokenContract = ` +pub contract FungibleToken { + + pub resource interface Provider { + pub fun withdraw(amount: Int): @Vault { + pre { + amount > 0: + "Withdrawal amount must be positive" + } + post { + result.balance == amount: + "Incorrect amount returned" + } + } + } + + pub resource interface Receiver { + pub balance: Int + + init(balance: Int) { + pre { + balance >= 0: + "Initial balance must be non-negative" + } + post { + self.balance == balance: + "Balance must be initialized to the initial balance" + } + } + + pub fun deposit(from: @Receiver) { + pre { + from.balance > 0: + "Deposit balance needs to be positive!" + } + post { + self.balance == before(self.balance) + before(from.balance): + "Incorrect amount removed" + } + } + } + + pub resource Vault: Provider, Receiver { + + pub var balance: Int + + init(balance: Int) { + self.balance = balance + } + + pub fun withdraw(amount: Int): @Vault { + self.balance = self.balance - amount + return <-create Vault(balance: amount) + } + + // transfer combines withdraw and deposit into one function call + pub fun transfer(to: &Receiver, amount: Int) { + pre { + amount <= self.balance: + "Insufficient funds" + } + post { + self.balance == before(self.balance) - amount: + "Incorrect amount removed" + } + to.deposit(from: <-self.withdraw(amount: amount)) + } + + pub fun deposit(from: @Receiver) { + self.balance = self.balance + from.balance + destroy from + } + + pub fun createEmptyVault(): @Vault { + return <-create Vault(balance: 0) + } + } + + pub fun createEmptyVault(): @Vault { + return <-create Vault(balance: 0) + } + + pub resource VaultMinter { + pub fun mintTokens(amount: Int, recipient: &Receiver) { + recipient.deposit(from: <-create Vault(balance: amount)) + } + } + + init() { + let oldVault <- self.account.storage[Vault] <- create Vault(balance: 30) + destroy oldVault + + let oldMinter <- self.account.storage[VaultMinter] <- create VaultMinter() + destroy oldMinter + } +} +` + +type testMemoryGauge struct { + meter map[common.MemoryKind]uint64 +} + +func (g *testMemoryGauge) MeterMemory(usage common.MemoryUsage) error { + g.meter[usage.Kind] += usage.Amount + return nil +} + +func BenchmarkParseFungibleToken(b *testing.B) { + + code := []byte(fungibleTokenContract) + + b.Run("Without memory metering", func(b *testing.B) { + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + _, err := ParseProgram(nil, code, Config{}) + if err != nil { + b.Fatal(err) + } + } + }) + + b.Run("With memory metering", func(b *testing.B) { + meter := &testMemoryGauge{ + meter: make(map[common.MemoryKind]uint64), + } + + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + _, err := ParseProgram(meter, code, Config{}) + if err != nil { + b.Fatal(err) + } + } + }) +} diff --git a/runtime/old_parser/comment.go b/runtime/old_parser/comment.go new file mode 100644 index 0000000000..33a3d4230b --- /dev/null +++ b/runtime/old_parser/comment.go @@ -0,0 +1,64 @@ +/* + * Cadence - The resource-oriented smart contract programming language + * + * Copyright Dapper Labs, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package old_parser + +import ( + "github.com/onflow/cadence/runtime/parser/lexer" +) + +func (p *parser) parseBlockComment() (endToken lexer.Token, ok bool) { + var depth int + + for { + switch p.current.Type { + case lexer.TokenBlockCommentStart: + p.next() + depth++ + + case lexer.TokenBlockCommentContent: + p.next() + + case lexer.TokenBlockCommentEnd: + endToken = p.current + // Skip the comment end (`*/`) + p.next() + ok = true + depth-- + if depth == 0 { + return + } + + case lexer.TokenEOF: + p.reportSyntaxError( + "missing comment end %s", + lexer.TokenBlockCommentEnd, + ) + ok = false + return + + default: + p.reportSyntaxError( + "unexpected token %s in block comment", + p.current.Type, + ) + ok = false + return + } + } +} diff --git a/runtime/old_parser/declaration.go b/runtime/old_parser/declaration.go new file mode 100644 index 0000000000..2edab477fb --- /dev/null +++ b/runtime/old_parser/declaration.go @@ -0,0 +1,1578 @@ +/* + * Cadence - The resource-oriented smart contract programming language + * + * Copyright Dapper Labs, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package old_parser + +import ( + "encoding/hex" + "strconv" + "strings" + + "github.com/onflow/cadence/runtime/ast" + "github.com/onflow/cadence/runtime/common" + "github.com/onflow/cadence/runtime/errors" + "github.com/onflow/cadence/runtime/parser/lexer" +) + +func parseDeclarations(p *parser, endTokenType lexer.TokenType) (declarations []ast.Declaration, err error) { + for { + _, docString := p.parseTrivia(triviaOptions{ + skipNewlines: true, + parseDocStrings: true, + }) + + switch p.current.Type { + case lexer.TokenSemicolon: + // Skip the semicolon + p.next() + continue + + case endTokenType, lexer.TokenEOF: + return + + default: + var declaration ast.Declaration + declaration, err = parseDeclaration(p, docString) + if err != nil { + return + } + + if declaration == nil { + return + } + + declarations = append(declarations, declaration) + } + } +} + +func parseDeclaration(p *parser, docString string) (ast.Declaration, error) { + + access := ast.AccessNotSpecified + var accessPos *ast.Position + + var staticPos *ast.Position + var nativePos *ast.Position + + staticModifierEnabled := p.config.StaticModifierEnabled + nativeModifierEnabled := p.config.NativeModifierEnabled + + for { + p.skipSpaceAndComments() + + switch p.current.Type { + case lexer.TokenPragma: + err := rejectAllModifiers(p, access, accessPos, staticPos, nativePos, common.DeclarationKindPragma) + if err != nil { + return nil, err + } + return parsePragmaDeclaration(p) + + case lexer.TokenIdentifier: + switch string(p.currentTokenSource()) { + case keywordLet, keywordVar: + err := rejectStaticAndNativeModifiers(p, staticPos, nativePos, common.DeclarationKindVariable) + if err != nil { + return nil, err + } + return parseVariableDeclaration(p, access, accessPos, docString) + + case keywordFun: + return parseFunctionDeclaration( + p, + false, + access, + accessPos, + staticPos, + nativePos, + docString, + ) + + case keywordImport: + err := rejectStaticAndNativeModifiers(p, staticPos, nativePos, common.DeclarationKindImport) + if err != nil { + return nil, err + } + return parseImportDeclaration(p) + + case keywordEvent: + err := rejectStaticAndNativeModifiers(p, staticPos, nativePos, common.DeclarationKindEvent) + if err != nil { + return nil, err + } + return parseEventDeclaration(p, access, accessPos, docString) + + case keywordStruct: + err := rejectStaticAndNativeModifiers(p, staticPos, nativePos, common.DeclarationKindStructure) + if err != nil { + return nil, err + } + return parseCompositeOrInterfaceDeclaration(p, access, accessPos, docString) + + case keywordResource: + err := rejectStaticAndNativeModifiers(p, staticPos, nativePos, common.DeclarationKindResource) + if err != nil { + return nil, err + } + return parseCompositeOrInterfaceDeclaration(p, access, accessPos, docString) + + case keywordAttachment: + err := rejectStaticAndNativeModifiers(p, staticPos, nativePos, common.DeclarationKindAttachment) + if err != nil { + return nil, err + } + return parseAttachmentDeclaration(p, access, accessPos, docString) + + case keywordContract: + err := rejectStaticAndNativeModifiers(p, staticPos, nativePos, common.DeclarationKindContract) + if err != nil { + return nil, err + } + return parseCompositeOrInterfaceDeclaration(p, access, accessPos, docString) + + case keywordEnum: + err := rejectStaticAndNativeModifiers(p, staticPos, nativePos, common.DeclarationKindEnum) + if err != nil { + return nil, err + } + return parseCompositeOrInterfaceDeclaration(p, access, accessPos, docString) + + case KeywordTransaction: + err := rejectAllModifiers(p, access, accessPos, staticPos, nativePos, common.DeclarationKindTransaction) + if err != nil { + return nil, err + } + return parseTransactionDeclaration(p, docString) + + case keywordPriv, keywordPub, keywordAccess: + if access != ast.AccessNotSpecified { + return nil, p.syntaxError("invalid second access modifier") + } + if staticModifierEnabled && staticPos != nil { + return nil, p.syntaxError("invalid access modifier after static modifier") + } + if nativeModifierEnabled && nativePos != nil { + return nil, p.syntaxError("invalid access modifier after native modifier") + } + pos := p.current.StartPos + accessPos = &pos + var err error + access, err = parseAccess(p) + if err != nil { + return nil, err + } + + continue + + case keywordStatic: + if !staticModifierEnabled { + break + } + + if staticPos != nil { + return nil, p.syntaxError("invalid second static modifier") + } + if nativeModifierEnabled && nativePos != nil { + return nil, p.syntaxError("invalid static modifier after native modifier") + } + pos := p.current.StartPos + staticPos = &pos + p.next() + continue + + case keywordNative: + if !nativeModifierEnabled { + break + } + + if nativePos != nil { + return nil, p.syntaxError("invalid second native modifier") + } + pos := p.current.StartPos + nativePos = &pos + p.next() + continue + } + } + + return nil, nil + } +} + +var enumeratedAccessModifierKeywords = common.EnumerateWords( + []string{ + strconv.Quote(keywordAll), + strconv.Quote(keywordAccount), + strconv.Quote(keywordContract), + strconv.Quote(keywordSelf), + }, + "or", +) + +// parseAccess parses an access modifier +// +// access +// : 'priv' +// | 'pub' ( '(' 'set' ')' )? +// | 'access' '(' ( 'self' | 'contract' | 'account' | 'all' ) ')' +func parseAccess(p *parser) (ast.PrimitiveAccess, error) { + + switch string(p.currentTokenSource()) { + case keywordPriv: + // Skip the `priv` keyword + p.next() + return ast.AccessSelf, nil + + case keywordPub: + // Skip the `pub` keyword + p.nextSemanticToken() + if !p.current.Is(lexer.TokenParenOpen) { + return ast.AccessAll, nil + } + + // Skip the opening paren + p.nextSemanticToken() + + if !p.current.Is(lexer.TokenIdentifier) { + return ast.AccessNotSpecified, p.syntaxError( + "expected keyword %q, got %s", + keywordSet, + p.current.Type, + ) + } + + keyword := p.currentTokenSource() + if string(keyword) != keywordSet { + return ast.AccessNotSpecified, p.syntaxError( + "expected keyword %q, got %q", + keywordSet, + keyword, + ) + } + + // Skip the `set` keyword + p.nextSemanticToken() + + _, err := p.mustOne(lexer.TokenParenClose) + if err != nil { + return ast.AccessNotSpecified, err + } + + return ast.PubSettableLegacy, nil + + case keywordAccess: + // Skip the `access` keyword + p.nextSemanticToken() + + _, err := p.mustOne(lexer.TokenParenOpen) + if err != nil { + return ast.AccessNotSpecified, err + } + + p.skipSpaceAndComments() + + if !p.current.Is(lexer.TokenIdentifier) { + return ast.AccessNotSpecified, p.syntaxError( + "expected keyword %s, got %s", + enumeratedAccessModifierKeywords, + p.current.Type, + ) + } + + var access ast.PrimitiveAccess + + keyword := p.currentTokenSource() + switch string(keyword) { + case keywordAll: + access = ast.AccessAll + + case keywordAccount: + access = ast.AccessAccount + + case keywordContract: + access = ast.AccessContract + + case keywordSelf: + access = ast.AccessSelf + + default: + return ast.AccessNotSpecified, p.syntaxError( + "expected keyword %s, got %q", + enumeratedAccessModifierKeywords, + keyword, + ) + } + + // Skip the keyword + p.nextSemanticToken() + + _, err = p.mustOne(lexer.TokenParenClose) + if err != nil { + return ast.AccessNotSpecified, err + } + + return access, nil + + default: + return ast.AccessNotSpecified, errors.NewUnreachableError() + } +} + +// parseVariableDeclaration parses a variable declaration. +// +// variableKind : 'var' | 'let' +// +// variableDeclaration : +// variableKind identifier ( ':' typeAnnotation )? +// transfer expression +// ( transfer expression )? +func parseVariableDeclaration( + p *parser, + access ast.Access, + accessPos *ast.Position, + docString string, +) (*ast.VariableDeclaration, error) { + + startPos := p.current.StartPos + if accessPos != nil { + startPos = *accessPos + } + + isLet := string(p.currentTokenSource()) == keywordLet + + // Skip the `let` or `var` keyword + p.nextSemanticToken() + if !p.current.Is(lexer.TokenIdentifier) { + return nil, p.syntaxError( + "expected identifier after start of variable declaration, got %s", + p.current.Type, + ) + } + + identifier := p.tokenToIdentifier(p.current) + + // Skip the identifier + p.nextSemanticToken() + + var typeAnnotation *ast.TypeAnnotation + var err error + + if p.current.Is(lexer.TokenColon) { + // Skip the colon + p.nextSemanticToken() + + typeAnnotation, err = parseTypeAnnotation(p) + if err != nil { + return nil, err + } + } + + p.skipSpaceAndComments() + transfer := parseTransfer(p) + if transfer == nil { + return nil, p.syntaxError("expected transfer") + } + + value, err := parseExpression(p, lowestBindingPower) + if err != nil { + return nil, err + } + + p.skipSpaceAndComments() + + secondTransfer := parseTransfer(p) + var secondValue ast.Expression + if secondTransfer != nil { + secondValue, err = parseExpression(p, lowestBindingPower) + if err != nil { + return nil, err + } + } + + variableDeclaration := ast.NewVariableDeclaration( + p.memoryGauge, + access, + isLet, + identifier, + typeAnnotation, + value, + transfer, + startPos, + secondTransfer, + secondValue, + docString, + ) + + castingExpression, leftIsCasting := value.(*ast.CastingExpression) + if leftIsCasting { + castingExpression.ParentVariableDeclaration = variableDeclaration + } + + return variableDeclaration, nil +} + +// parseTransfer parses a transfer. +// +// transfer : '=' | '<-' | '<-!' +func parseTransfer(p *parser) *ast.Transfer { + var operation ast.TransferOperation + + switch p.current.Type { + case lexer.TokenEqual: + operation = ast.TransferOperationCopy + + case lexer.TokenLeftArrow: + operation = ast.TransferOperationMove + + case lexer.TokenLeftArrowExclamation: + operation = ast.TransferOperationMoveForced + } + + if operation == ast.TransferOperationUnknown { + return nil + } + + pos := p.current.StartPos + + p.next() + + return ast.NewTransfer( + p.memoryGauge, + operation, + pos, + ) +} + +func parsePragmaDeclaration(p *parser) (*ast.PragmaDeclaration, error) { + startPos := p.current.StartPosition() + p.next() + + expr, err := parseExpression(p, lowestBindingPower) + if err != nil { + return nil, err + } + + return ast.NewPragmaDeclaration( + p.memoryGauge, + expr, + ast.NewRange( + p.memoryGauge, + startPos, + expr.EndPosition(p.memoryGauge), + ), + ), nil +} + +// parseImportDeclaration parses an import declaration +// +// importDeclaration : +// 'import' +// ( identifier (',' identifier)* 'from' )? +// ( string | hexadecimalLiteral | identifier ) +func parseImportDeclaration(p *parser) (*ast.ImportDeclaration, error) { + + startPosition := p.current.StartPos + + var identifiers []ast.Identifier + + var location common.Location + var locationPos ast.Position + var endPos ast.Position + + parseStringOrAddressLocation := func() { + locationPos = p.current.StartPos + endPos = p.current.EndPos + + switch p.current.Type { + case lexer.TokenString: + literal := p.currentTokenSource() + parsedString := parseStringLiteral(p, literal) + location = common.NewStringLocation(p.memoryGauge, parsedString) + + case lexer.TokenHexadecimalIntegerLiteral: + location = parseHexadecimalLocation(p) + + default: + panic(errors.NewUnreachableError()) + } + + // Skip the location + p.next() + } + + setIdentifierLocation := func(identifier ast.Identifier) { + location = common.IdentifierLocation(identifier.Identifier) + locationPos = identifier.Pos + endPos = identifier.EndPosition(p.memoryGauge) + } + + parseLocation := func() error { + switch p.current.Type { + case lexer.TokenString, lexer.TokenHexadecimalIntegerLiteral: + parseStringOrAddressLocation() + + case lexer.TokenIdentifier: + identifier := p.tokenToIdentifier(p.current) + setIdentifierLocation(identifier) + p.next() + + default: + return p.syntaxError( + "unexpected token in import declaration: got %s, expected string, address, or identifier", + p.current.Type, + ) + } + + return nil + } + + parseMoreIdentifiers := func() error { + expectCommaOrFrom := false + + atEnd := false + for !atEnd { + p.nextSemanticToken() + + switch p.current.Type { + case lexer.TokenComma: + if !expectCommaOrFrom { + return p.syntaxError( + "expected %s or keyword %q, got %s", + lexer.TokenIdentifier, + keywordFrom, + p.current.Type, + ) + } + expectCommaOrFrom = false + + case lexer.TokenIdentifier: + + keyword := p.currentTokenSource() + if string(keyword) == keywordFrom { + if expectCommaOrFrom { + atEnd = true + + // Skip the `from` keyword + p.nextSemanticToken() + + err := parseLocation() + if err != nil { + return err + } + + break + } + + if !isNextTokenCommaOrFrom(p) { + return p.syntaxError( + "expected %s, got keyword %q", + lexer.TokenIdentifier, + keyword, + ) + } + + // If the next token is either comma or 'from' token, then fall through + // and process the current 'from' token as an identifier. + } + + identifier := p.tokenToIdentifier(p.current) + identifiers = append(identifiers, identifier) + + expectCommaOrFrom = true + + case lexer.TokenEOF: + return p.syntaxError( + "unexpected end in import declaration: expected %s or %s", + lexer.TokenIdentifier, + lexer.TokenComma, + ) + + default: + return p.syntaxError( + "unexpected token in import declaration: got %s, expected keyword %q or %s", + p.current.Type, + keywordFrom, + lexer.TokenComma, + ) + } + } + + return nil + } + + maybeParseFromIdentifier := func(identifier ast.Identifier) error { + // The current identifier is maybe the `from` keyword, + // in which case the given (previous) identifier was + // an imported identifier and not the import location. + // + // If it is not the `from` keyword, + // the given (previous) identifier is the import location. + + if string(p.currentTokenSource()) == keywordFrom { + identifiers = append(identifiers, identifier) + // Skip the `from` keyword + p.nextSemanticToken() + + err := parseLocation() + if err != nil { + return err + } + } else { + setIdentifierLocation(identifier) + } + + return nil + } + + // Skip the `import` keyword + p.nextSemanticToken() + + switch p.current.Type { + case lexer.TokenString, lexer.TokenHexadecimalIntegerLiteral: + parseStringOrAddressLocation() + + case lexer.TokenIdentifier: + identifier := p.tokenToIdentifier(p.current) + // Skip the identifier + p.nextSemanticToken() + + switch p.current.Type { + case lexer.TokenComma: + // The previous identifier is an imported identifier, + // not the import location + identifiers = append(identifiers, identifier) + err := parseMoreIdentifiers() + if err != nil { + return nil, err + } + case lexer.TokenIdentifier: + err := maybeParseFromIdentifier(identifier) + if err != nil { + return nil, err + } + case lexer.TokenEOF: + // The previous identifier is the identifier location + setIdentifierLocation(identifier) + + default: + return nil, p.syntaxError( + "unexpected token in import declaration: got %s, expected keyword %q or %s", + p.current.Type, + keywordFrom, + lexer.TokenComma, + ) + } + + case lexer.TokenEOF: + return nil, p.syntaxError("unexpected end in import declaration: expected string, address, or identifier") + + default: + return nil, p.syntaxError( + "unexpected token in import declaration: got %s, expected string, address, or identifier", + p.current.Type, + ) + } + + return ast.NewImportDeclaration( + p.memoryGauge, + identifiers, + location, + ast.NewRange( + p.memoryGauge, + startPosition, + endPos, + ), + locationPos, + ), nil +} + +// isNextTokenCommaOrFrom check whether the token to follow is a comma or a from token. +func isNextTokenCommaOrFrom(p *parser) bool { + current := p.current + cursor := p.tokens.Cursor() + defer func() { + p.current = current + p.tokens.Revert(cursor) + }() + + // skip the current token + p.nextSemanticToken() + + // Lookahead the next token + switch p.current.Type { + case lexer.TokenIdentifier: + return string(p.currentTokenSource()) == keywordFrom + + case lexer.TokenComma: + return true + } + + return false +} + +func parseHexadecimalLocation(p *parser) common.AddressLocation { + // TODO: improve + literal := string(p.currentTokenSource()) + + bytes := []byte(strings.ReplaceAll(literal[2:], "_", "")) + + length := len(bytes) + if length%2 == 1 { + bytes = append([]byte{'0'}, bytes...) + length++ + } + + rawAddress := make([]byte, hex.DecodedLen(length)) + _, err := hex.Decode(rawAddress, bytes) + if err != nil { + // unreachable, hex literal should always be valid + panic(errors.NewUnexpectedErrorFromCause(err)) + } + + address, err := common.BytesToAddress(rawAddress) + if err != nil { + // Any returned error is a syntax error. e.g: Address too large error. + p.reportSyntaxError(err.Error()) + } + + return common.NewAddressLocation(p.memoryGauge, address, "") +} + +// parseEventDeclaration parses an event declaration. +// +// eventDeclaration : 'event' identifier parameterList +func parseEventDeclaration( + p *parser, + access ast.Access, + accessPos *ast.Position, + docString string, +) (*ast.CompositeDeclaration, error) { + + startPos := p.current.StartPos + if accessPos != nil { + startPos = *accessPos + } + + // Skip the `event` keyword + p.nextSemanticToken() + if !p.current.Is(lexer.TokenIdentifier) { + return nil, p.syntaxError( + "expected identifier after start of event declaration, got %s", + p.current.Type, + ) + } + + identifier := p.tokenToIdentifier(p.current) + // Skip the identifier + p.next() + + parameterList, err := parseParameterList(p) + if err != nil { + return nil, err + } + + initializer := ast.NewSpecialFunctionDeclaration( + p.memoryGauge, + common.DeclarationKindInitializer, + ast.NewFunctionDeclaration( + p.memoryGauge, + ast.AccessNotSpecified, + ast.FunctionPurityUnspecified, + false, + false, + ast.NewEmptyIdentifier(p.memoryGauge, ast.EmptyPosition), + nil, + parameterList, + nil, + nil, + parameterList.StartPos, + "", + ), + ) + + members := ast.NewMembers( + p.memoryGauge, + []ast.Declaration{ + initializer, + }, + ) + + return ast.NewCompositeDeclaration( + p.memoryGauge, + access, + common.CompositeKindEvent, + identifier, + nil, + members, + docString, + ast.NewRange( + p.memoryGauge, + startPos, + parameterList.EndPos, + ), + ), nil +} + +// parseCompositeKind parses a composite kind. +// +// compositeKind : 'struct' | 'resource' | 'contract' | 'enum' +func parseCompositeKind(p *parser) common.CompositeKind { + + if p.current.Is(lexer.TokenIdentifier) { + switch string(p.currentTokenSource()) { + case keywordStruct: + return common.CompositeKindStructure + + case keywordResource: + return common.CompositeKindResource + + case keywordContract: + return common.CompositeKindContract + + case keywordEnum: + return common.CompositeKindEnum + } + } + + return common.CompositeKindUnknown +} + +// parseFieldWithVariableKind parses a field which has a variable kind. +// +// variableKind : 'var' | 'let' +// +// field : variableKind identifier ':' typeAnnotation +func parseFieldWithVariableKind( + p *parser, + access ast.Access, + accessPos *ast.Position, + staticPos *ast.Position, + nativePos *ast.Position, + docString string, +) (*ast.FieldDeclaration, error) { + + startPos := ast.EarliestPosition(p.current.StartPos, accessPos, staticPos, nativePos) + + var variableKind ast.VariableKind + switch string(p.currentTokenSource()) { + case keywordLet: + variableKind = ast.VariableKindConstant + + case keywordVar: + variableKind = ast.VariableKindVariable + } + + // Skip the `let` or `var` keyword + p.nextSemanticToken() + if !p.current.Is(lexer.TokenIdentifier) { + return nil, p.syntaxError( + "expected identifier after start of field declaration, got %s", + p.current.Type, + ) + } + + identifier := p.tokenToIdentifier(p.current) + // Skip the identifier + p.nextSemanticToken() + + _, err := p.mustOne(lexer.TokenColon) + if err != nil { + return nil, err + } + + p.skipSpaceAndComments() + + typeAnnotation, err := parseTypeAnnotation(p) + if err != nil { + return nil, err + } + + return ast.NewFieldDeclaration( + p.memoryGauge, + access, + staticPos != nil, + nativePos != nil, + variableKind, + identifier, + typeAnnotation, + docString, + ast.NewRange( + p.memoryGauge, + startPos, + typeAnnotation.EndPosition(p.memoryGauge), + ), + ), nil +} + +func parseConformances(p *parser) ([]*ast.NominalType, error) { + var conformances []*ast.NominalType + var err error + + if p.current.Is(lexer.TokenColon) { + // Skip the colon + p.next() + + conformances, _, err = parseNominalTypes(p, lexer.TokenBraceOpen) + if err != nil { + return nil, err + } + + if len(conformances) < 1 { + return nil, p.syntaxError( + "expected at least one conformance after %s", + lexer.TokenColon, + ) + } + } + + p.skipSpaceAndComments() + return conformances, nil +} + +// parseCompositeOrInterfaceDeclaration parses an event declaration. +// +// conformances : ':' nominalType ( ',' nominalType )* +// +// compositeDeclaration : compositeKind identifier conformances? +// '{' membersAndNestedDeclarations '}' +// +// interfaceDeclaration : compositeKind 'interface' identifier conformances? +// '{' membersAndNestedDeclarations '}' +func parseCompositeOrInterfaceDeclaration( + p *parser, + access ast.Access, + accessPos *ast.Position, + docString string, +) (ast.Declaration, error) { + + startPos := p.current.StartPos + if accessPos != nil { + startPos = *accessPos + } + + compositeKind := parseCompositeKind(p) + + // Skip the composite kind keyword + p.next() + + var isInterface bool + var identifier ast.Identifier + + for { + p.skipSpaceAndComments() + if !p.current.Is(lexer.TokenIdentifier) { + return nil, p.syntaxError( + "expected %s, got %s", + lexer.TokenIdentifier, + p.current.Type, + ) + } + + wasInterface := isInterface + + if string(p.currentTokenSource()) == keywordInterface { + isInterface = true + if wasInterface { + return nil, p.syntaxError( + "expected interface name, got keyword %q", + keywordInterface, + ) + } + // Skip the `interface` keyword + p.next() + continue + } else { + identifier = p.tokenToIdentifier(p.current) + // Skip the identifier + p.next() + break + } + } + + p.skipSpaceAndComments() + + conformances, err := parseConformances(p) + if err != nil { + return nil, err + } + + _, err = p.mustOne(lexer.TokenBraceOpen) + if err != nil { + return nil, err + } + + members, err := parseMembersAndNestedDeclarations(p, lexer.TokenBraceClose) + if err != nil { + return nil, err + } + + p.skipSpaceAndComments() + + endToken, err := p.mustOne(lexer.TokenBraceClose) + if err != nil { + return nil, err + } + + declarationRange := ast.NewRange( + p.memoryGauge, + startPos, + endToken.EndPos, + ) + + if isInterface { + // TODO: remove once interface conformances are supported + if len(conformances) > 0 { + // TODO: improve + return nil, p.syntaxError("unexpected conformances") + } + + return ast.NewInterfaceDeclaration( + p.memoryGauge, + access, + compositeKind, + identifier, + []*ast.NominalType{}, + members, + docString, + declarationRange, + ), nil + } else { + return ast.NewCompositeDeclaration( + p.memoryGauge, + access, + compositeKind, + identifier, + conformances, + members, + docString, + declarationRange, + ), nil + } +} + +func parseAttachmentDeclaration( + p *parser, + access ast.Access, + accessPos *ast.Position, + docString string, +) (ast.Declaration, error) { + startPos := p.current.StartPos + if accessPos != nil { + startPos = *accessPos + } + + // Skip the `attachment` keyword + p.nextSemanticToken() + + identifier, err := p.mustIdentifier() + if err != nil { + return nil, err + } + + p.skipSpaceAndComments() + + if !p.isToken(p.current, lexer.TokenIdentifier, keywordFor) { + return nil, p.syntaxError( + "expected 'for', got %s", + p.current.Type, + ) + } + + // skip the `for` keyword + p.nextSemanticToken() + + if !p.current.Is(lexer.TokenIdentifier) { + return nil, p.syntaxError( + "expected %s, got %s", + lexer.TokenIdentifier, + p.current.Type, + ) + } + + baseType, err := parseType(p, lowestBindingPower) + baseNominalType, ok := baseType.(*ast.NominalType) + if !ok { + p.reportSyntaxError( + "expected nominal type, got %s", + baseType, + ) + } + if err != nil { + return nil, err + } + + conformances, err := parseConformances(p) + if err != nil { + return nil, err + } + + _, err = p.mustOne(lexer.TokenBraceOpen) + if err != nil { + return nil, err + } + + members, err := parseMembersAndNestedDeclarations(p, lexer.TokenBraceClose) + if err != nil { + return nil, err + } + + p.skipSpaceAndComments() + + endToken, err := p.mustOne(lexer.TokenBraceClose) + if err != nil { + return nil, err + } + + declarationRange := ast.NewRange( + p.memoryGauge, + startPos, + endToken.EndPos, + ) + + return ast.NewAttachmentDeclaration( + p.memoryGauge, + access, + identifier, + baseNominalType, + conformances, + members, + docString, + declarationRange, + ), nil +} + +// parseMembersAndNestedDeclarations parses composite or interface members, +// and nested declarations. +// +// membersAndNestedDeclarations : ( memberOrNestedDeclaration ';'* )* +func parseMembersAndNestedDeclarations(p *parser, endTokenType lexer.TokenType) (*ast.Members, error) { + + var declarations []ast.Declaration + + for { + _, docString := p.parseTrivia(triviaOptions{ + skipNewlines: true, + parseDocStrings: true, + }) + + switch p.current.Type { + case lexer.TokenSemicolon: + // Skip the semicolon + p.next() + continue + + case endTokenType, lexer.TokenEOF: + return ast.NewMembers(p.memoryGauge, declarations), nil + + default: + memberOrNestedDeclaration, err := parseMemberOrNestedDeclaration(p, docString) + if err != nil { + return nil, err + } + + if memberOrNestedDeclaration == nil { + return ast.NewMembers(p.memoryGauge, declarations), nil + } + + declarations = append(declarations, memberOrNestedDeclaration) + } + } +} + +// parseMemberOrNestedDeclaration parses a composite or interface member, +// or a declaration nested in it. +// +// memberOrNestedDeclaration : field +// | specialFunctionDeclaration +// | functionDeclaration +// | interfaceDeclaration +// | compositeDeclaration +// | eventDeclaration +// | enumCase +// | pragmaDeclaration +func parseMemberOrNestedDeclaration(p *parser, docString string) (ast.Declaration, error) { + + const functionBlockIsOptional = true + + access := ast.AccessNotSpecified + var accessPos *ast.Position + + var staticPos *ast.Position + var nativePos *ast.Position + + var previousIdentifierToken *lexer.Token + + staticModifierEnabled := p.config.StaticModifierEnabled + nativeModifierEnabled := p.config.NativeModifierEnabled + + for { + p.skipSpaceAndComments() + + switch p.current.Type { + case lexer.TokenIdentifier: + + switch string(p.currentTokenSource()) { + case keywordLet, keywordVar: + return parseFieldWithVariableKind( + p, + access, + accessPos, + staticPos, + nativePos, + docString, + ) + + case keywordCase: + err := rejectStaticAndNativeModifiers(p, staticPos, nativePos, common.DeclarationKindEnumCase) + if err != nil { + return nil, err + } + return parseEnumCase(p, access, accessPos, docString) + + case keywordFun: + return parseFunctionDeclaration( + p, + functionBlockIsOptional, + access, + accessPos, + staticPos, + nativePos, + docString, + ) + + case keywordEvent: + err := rejectStaticAndNativeModifiers(p, staticPos, nativePos, common.DeclarationKindEvent) + if err != nil { + return nil, err + } + return parseEventDeclaration(p, access, accessPos, docString) + + case keywordStruct: + err := rejectStaticAndNativeModifiers(p, staticPos, nativePos, common.DeclarationKindStructure) + if err != nil { + return nil, err + } + return parseCompositeOrInterfaceDeclaration(p, access, accessPos, docString) + + case keywordResource: + err := rejectStaticAndNativeModifiers(p, staticPos, nativePos, common.DeclarationKindResource) + if err != nil { + return nil, err + } + return parseCompositeOrInterfaceDeclaration(p, access, accessPos, docString) + + case keywordContract: + err := rejectStaticAndNativeModifiers(p, staticPos, nativePos, common.DeclarationKindContract) + if err != nil { + return nil, err + } + return parseCompositeOrInterfaceDeclaration(p, access, accessPos, docString) + + case keywordEnum: + err := rejectStaticAndNativeModifiers(p, staticPos, nativePos, common.DeclarationKindEnum) + if err != nil { + return nil, err + } + return parseCompositeOrInterfaceDeclaration(p, access, accessPos, docString) + + case keywordAttachment: + return parseAttachmentDeclaration(p, access, accessPos, docString) + + case keywordPriv, keywordPub, keywordAccess: + if access != ast.AccessNotSpecified { + return nil, p.syntaxError("invalid second access modifier") + } + if staticModifierEnabled && staticPos != nil { + return nil, p.syntaxError("invalid access modifier after static modifier") + } + if nativeModifierEnabled && nativePos != nil { + return nil, p.syntaxError("invalid access modifier after native modifier") + } + pos := p.current.StartPos + accessPos = &pos + var err error + access, err = parseAccess(p) + if err != nil { + return nil, err + } + continue + + case keywordStatic: + if !staticModifierEnabled { + break + } + + if staticPos != nil { + return nil, p.syntaxError("invalid second static modifier") + } + if nativeModifierEnabled && nativePos != nil { + return nil, p.syntaxError("invalid static modifier after native modifier") + } + pos := p.current.StartPos + staticPos = &pos + p.next() + continue + + case keywordNative: + if !nativeModifierEnabled { + break + } + + if nativePos != nil { + return nil, p.syntaxError("invalid second native modifier") + } + pos := p.current.StartPos + nativePos = &pos + p.next() + continue + } + + if previousIdentifierToken != nil { + return nil, p.syntaxError("unexpected %s", p.current.Type) + } + + t := p.current + previousIdentifierToken = &t + // Skip the identifier + p.next() + continue + + case lexer.TokenPragma: + if previousIdentifierToken != nil { + return nil, NewSyntaxError( + previousIdentifierToken.StartPos, + "unexpected token: %s", + previousIdentifierToken.Type, + ) + } + err := rejectAllModifiers(p, access, accessPos, staticPos, nativePos, common.DeclarationKindPragma) + if err != nil { + return nil, err + } + return parsePragmaDeclaration(p) + + case lexer.TokenColon: + if previousIdentifierToken == nil { + return nil, p.syntaxError("unexpected %s", p.current.Type) + } + + identifier := p.tokenToIdentifier(*previousIdentifierToken) + return parseFieldDeclarationWithoutVariableKind( + p, + access, + accessPos, + staticPos, + nativePos, + identifier, + docString, + ) + + case lexer.TokenParenOpen: + if previousIdentifierToken == nil { + return nil, p.syntaxError("unexpected %s", p.current.Type) + } + + identifier := p.tokenToIdentifier(*previousIdentifierToken) + return parseSpecialFunctionDeclaration( + p, + functionBlockIsOptional, + access, + accessPos, + staticPos, + nativePos, + identifier, + docString, + ) + } + + return nil, nil + } +} + +func rejectAllModifiers( + p *parser, + access ast.Access, + accessPos *ast.Position, + staticPos *ast.Position, + nativePos *ast.Position, + kind common.DeclarationKind, +) error { + if access != ast.AccessNotSpecified { + return NewSyntaxError(*accessPos, "invalid access modifier for %s", kind.Name()) + } + return rejectStaticAndNativeModifiers(p, staticPos, nativePos, kind) +} + +func rejectStaticAndNativeModifiers( + p *parser, + staticPos *ast.Position, + nativePos *ast.Position, + kind common.DeclarationKind, +) error { + if p.config.StaticModifierEnabled && staticPos != nil { + return NewSyntaxError(*staticPos, "invalid static modifier for %s", kind.Name()) + } + if p.config.NativeModifierEnabled && nativePos != nil { + return NewSyntaxError(*nativePos, "invalid native modifier for %s", kind.Name()) + } + return nil +} + +func parseFieldDeclarationWithoutVariableKind( + p *parser, + access ast.Access, + accessPos *ast.Position, + staticPos *ast.Position, + nativePos *ast.Position, + identifier ast.Identifier, + docString string, +) (*ast.FieldDeclaration, error) { + + startPos := ast.EarliestPosition(identifier.Pos, accessPos, staticPos, nativePos) + + _, err := p.mustOne(lexer.TokenColon) + if err != nil { + return nil, err + } + + p.skipSpaceAndComments() + + typeAnnotation, err := parseTypeAnnotation(p) + if err != nil { + return nil, err + } + + return ast.NewFieldDeclaration( + p.memoryGauge, + access, + staticPos != nil, + nativePos != nil, + ast.VariableKindNotSpecified, + identifier, + typeAnnotation, + docString, + ast.NewRange( + p.memoryGauge, + startPos, + typeAnnotation.EndPosition(p.memoryGauge), + ), + ), nil +} + +func parseSpecialFunctionDeclaration( + p *parser, + functionBlockIsOptional bool, + access ast.Access, + accessPos *ast.Position, + staticPos *ast.Position, + nativePos *ast.Position, + identifier ast.Identifier, + docString string, +) (*ast.SpecialFunctionDeclaration, error) { + + startPos := ast.EarliestPosition(identifier.Pos, accessPos, staticPos, nativePos) + + parameterList, returnTypeAnnotation, functionBlock, err := + parseFunctionParameterListAndRest(p, functionBlockIsOptional) + if err != nil { + return nil, err + } + + declarationKind := common.DeclarationKindUnknown + switch identifier.Identifier { + case keywordInit: + declarationKind = common.DeclarationKindInitializer + + case keywordDestroy: + declarationKind = common.DeclarationKindDestructorLegacy + + case keywordPrepare: + declarationKind = common.DeclarationKindPrepare + } + + if returnTypeAnnotation != nil { + var kindDescription string + if declarationKind != common.DeclarationKindUnknown { + kindDescription = declarationKind.Name() + } else { + kindDescription = "special function" + } + + p.report(NewSyntaxError( + returnTypeAnnotation.StartPos, + "invalid return type for %s", + kindDescription, + )) + } + + return ast.NewSpecialFunctionDeclaration( + p.memoryGauge, + declarationKind, + ast.NewFunctionDeclaration( + p.memoryGauge, + access, + ast.FunctionPurityUnspecified, + staticPos != nil, + nativePos != nil, + identifier, + nil, + parameterList, + nil, + functionBlock, + startPos, + docString, + ), + ), nil +} + +// parseEnumCase parses a field which has a variable kind. +// +// enumCase : 'case' identifier +func parseEnumCase( + p *parser, + access ast.Access, + accessPos *ast.Position, + docString string, +) (*ast.EnumCaseDeclaration, error) { + + startPos := p.current.StartPos + if accessPos != nil { + startPos = *accessPos + } + + // Skip the `enum` keyword + p.nextSemanticToken() + if !p.current.Is(lexer.TokenIdentifier) { + return nil, p.syntaxError( + "expected identifier after start of enum case declaration, got %s", + p.current.Type, + ) + } + + identifier := p.tokenToIdentifier(p.current) + // Skip the identifier + p.next() + + return ast.NewEnumCaseDeclaration( + p.memoryGauge, + access, + identifier, + docString, + startPos, + ), nil +} diff --git a/runtime/old_parser/declaration_test.go b/runtime/old_parser/declaration_test.go new file mode 100644 index 0000000000..909c626f2f --- /dev/null +++ b/runtime/old_parser/declaration_test.go @@ -0,0 +1,7083 @@ +/* + * Cadence - The resource-oriented smart contract programming language + * + * Copyright Dapper Labs, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package old_parser + +import ( + "fmt" + "math/big" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/onflow/cadence/runtime/ast" + "github.com/onflow/cadence/runtime/common" + "github.com/onflow/cadence/runtime/tests/utils" +) + +func TestParseVariableDeclaration(t *testing.T) { + + t.Parallel() + + t.Run("var, no type annotation, copy, one value", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseDeclarations("var x = 1") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.VariableDeclaration{ + Access: ast.AccessNotSpecified, + IsConstant: false, + Identifier: ast.Identifier{ + Identifier: "x", + Pos: ast.Position{Line: 1, Column: 4, Offset: 4}, + }, + Value: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 8, Offset: 8}, + EndPos: ast.Position{Line: 1, Column: 8, Offset: 8}, + }, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationCopy, + Pos: ast.Position{Line: 1, Column: 6, Offset: 6}, + }, + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + result, + ) + }) + + t.Run("var, no type annotation, copy, one value, pub", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseDeclarations(" pub var x = 1") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.VariableDeclaration{ + Access: ast.AccessAll, + IsConstant: false, + Identifier: ast.Identifier{ + Identifier: "x", + Pos: ast.Position{Line: 1, Column: 9, Offset: 9}, + }, + Value: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 13, Offset: 13}, + EndPos: ast.Position{Line: 1, Column: 13, Offset: 13}, + }, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationCopy, + Pos: ast.Position{Line: 1, Column: 11, Offset: 11}, + }, + StartPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + }, + }, + result, + ) + }) + + t.Run("let, no type annotation, copy, one value", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseDeclarations("let x = 1") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.VariableDeclaration{ + Access: ast.AccessNotSpecified, + IsConstant: true, + Identifier: ast.Identifier{ + Identifier: "x", + Pos: ast.Position{Line: 1, Column: 4, Offset: 4}, + }, + Value: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 8, Offset: 8}, + EndPos: ast.Position{Line: 1, Column: 8, Offset: 8}, + }, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationCopy, + Pos: ast.Position{Line: 1, Column: 6, Offset: 6}, + }, + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + result, + ) + }) + + t.Run("let, no type annotation, move, one value", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseDeclarations("let x <- 1") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.VariableDeclaration{ + Access: ast.AccessNotSpecified, + IsConstant: true, + Identifier: ast.Identifier{ + Identifier: "x", + Pos: ast.Position{Line: 1, Column: 4, Offset: 4}, + }, + Value: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 9, Offset: 9}, + EndPos: ast.Position{Line: 1, Column: 9, Offset: 9}, + }, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationMove, + Pos: ast.Position{Line: 1, Column: 6, Offset: 6}, + }, + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + result, + ) + }) + + t.Run("let, resource type annotation, move, one value", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseDeclarations("let r2: @R <- r") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.VariableDeclaration{ + Access: ast.AccessNotSpecified, + IsConstant: true, + Identifier: ast.Identifier{ + Identifier: "r2", + Pos: ast.Position{Line: 1, Column: 4, Offset: 4}, + }, + TypeAnnotation: &ast.TypeAnnotation{ + IsResource: true, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "R", + Pos: ast.Position{Line: 1, Column: 9, Offset: 9}, + }, + }, + StartPos: ast.Position{Line: 1, Column: 8, Offset: 8}, + }, + Value: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "r", + Pos: ast.Position{Line: 1, Column: 14, Offset: 14}, + }, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationMove, + Pos: ast.Position{Line: 1, Column: 11, Offset: 11}, + }, + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + result, + ) + }) + + t.Run("var, no type annotation, copy, two values ", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseStatements("var x <- y <- z") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Statement{ + &ast.VariableDeclaration{ + Access: ast.AccessNotSpecified, + IsConstant: false, + Identifier: ast.Identifier{ + Identifier: "x", + Pos: ast.Position{Line: 1, Column: 4, Offset: 4}, + }, + Value: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "y", + Pos: ast.Position{Line: 1, Column: 9, Offset: 9}, + }, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationMove, + Pos: ast.Position{Line: 1, Column: 6, Offset: 6}, + }, + SecondValue: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "z", + Pos: ast.Position{Line: 1, Column: 14, Offset: 14}, + }, + }, + SecondTransfer: &ast.Transfer{ + Operation: ast.TransferOperationMove, + Pos: ast.Position{Line: 1, Column: 11, Offset: 11}, + }, + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + result, + ) + }) + + t.Run("static, enabled", func(t *testing.T) { + + t.Parallel() + + _, errs := ParseDeclarations( + nil, + []byte("static var x = 1"), + Config{ + StaticModifierEnabled: true, + }, + ) + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "invalid static modifier for variable", + Pos: ast.Position{Offset: 0, Line: 1, Column: 0}, + }, + }, + errs, + ) + }) + + t.Run("static, disabled", func(t *testing.T) { + + t.Parallel() + + _, errs := testParseDeclarations("static var x = 1") + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "unexpected token: identifier", + Pos: ast.Position{Offset: 0, Line: 1, Column: 0}, + }, + }, + errs, + ) + }) + + t.Run("native, enabled", func(t *testing.T) { + + t.Parallel() + + _, errs := ParseDeclarations( + nil, + []byte("native var x = 1"), + Config{ + NativeModifierEnabled: true, + }, + ) + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "invalid native modifier for variable", + Pos: ast.Position{Offset: 0, Line: 1, Column: 0}, + }, + }, + errs, + ) + }) + + t.Run("native, disabled", func(t *testing.T) { + + t.Parallel() + + _, errs := testParseDeclarations("native var x = 1") + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "unexpected token: identifier", + Pos: ast.Position{Offset: 0, Line: 1, Column: 0}, + }, + }, + errs, + ) + }) +} + +func TestParseParameterList(t *testing.T) { + + t.Parallel() + + parse := func(input string) (*ast.ParameterList, []error) { + return Parse( + nil, + []byte(input), + parseParameterList, + Config{}, + ) + } + + t.Run("empty", func(t *testing.T) { + + t.Parallel() + + result, errs := parse("()") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.ParameterList{ + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + }, + }, + result, + ) + }) + + t.Run("space", func(t *testing.T) { + + t.Parallel() + + result, errs := parse(" ( )") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.ParameterList{ + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + EndPos: ast.Position{Line: 1, Column: 5, Offset: 5}, + }, + }, + result, + ) + }) + + t.Run("one, without argument label", func(t *testing.T) { + + t.Parallel() + + result, errs := parse("( a : Int )") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.ParameterList{ + Parameters: []*ast.Parameter{ + { + Label: "", + Identifier: ast.Identifier{ + Identifier: "a", + Pos: ast.Position{Line: 1, Column: 2, Offset: 2}, + }, + TypeAnnotation: &ast.TypeAnnotation{ + IsResource: false, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Int", + Pos: ast.Position{Line: 1, Column: 6, Offset: 6}, + }, + }, + StartPos: ast.Position{Line: 1, Column: 6, Offset: 6}, + }, + StartPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 10, Offset: 10}, + }, + }, + result, + ) + }) + + t.Run("one, with argument label", func(t *testing.T) { + + t.Parallel() + + result, errs := parse("( a b : Int )") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.ParameterList{ + Parameters: []*ast.Parameter{ + { + Label: "a", + Identifier: ast.Identifier{ + Identifier: "b", + Pos: ast.Position{Line: 1, Column: 4, Offset: 4}, + }, + TypeAnnotation: &ast.TypeAnnotation{ + IsResource: false, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Int", + Pos: ast.Position{Line: 1, Column: 8, Offset: 8}, + }, + }, + StartPos: ast.Position{Line: 1, Column: 8, Offset: 8}, + }, + StartPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 12, Offset: 12}, + }, + }, + result, + ) + }) + + t.Run("two, with and without argument label", func(t *testing.T) { + + t.Parallel() + + result, errs := parse("( a b : Int , c : Int )") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.ParameterList{ + Parameters: []*ast.Parameter{ + { + Label: "a", + Identifier: ast.Identifier{ + Identifier: "b", + Pos: ast.Position{Line: 1, Column: 4, Offset: 4}, + }, + TypeAnnotation: &ast.TypeAnnotation{ + IsResource: false, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Int", + Pos: ast.Position{Line: 1, Column: 8, Offset: 8}, + }, + }, + StartPos: ast.Position{Line: 1, Column: 8, Offset: 8}, + }, + StartPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + }, + { + Label: "", + Identifier: ast.Identifier{ + Identifier: "c", + Pos: ast.Position{Line: 1, Column: 14, Offset: 14}, + }, + TypeAnnotation: &ast.TypeAnnotation{ + IsResource: false, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Int", + Pos: ast.Position{Line: 1, Column: 18, Offset: 18}, + }, + }, + StartPos: ast.Position{Line: 1, Column: 18, Offset: 18}, + }, + StartPos: ast.Position{Line: 1, Column: 14, Offset: 14}, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 22, Offset: 22}, + }, + }, + result, + ) + }) + + t.Run("two, with and without argument label, missing comma", func(t *testing.T) { + + t.Parallel() + + _, errs := parse("( a b : Int c : Int )") + utils.AssertEqualWithDiff(t, + []error{ + &MissingCommaInParameterListError{ + Pos: ast.Position{Offset: 14, Line: 1, Column: 14}, + }, + }, + errs, + ) + }) +} + +func TestParseFunctionDeclaration(t *testing.T) { + + t.Parallel() + + t.Run("without return type", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseDeclarations("fun foo () { }") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.FunctionDeclaration{ + Access: ast.AccessNotSpecified, + Identifier: ast.Identifier{ + Identifier: "foo", + Pos: ast.Position{Line: 1, Column: 4, Offset: 4}, + }, + ParameterList: &ast.ParameterList{ + Parameters: nil, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 8, Offset: 8}, + EndPos: ast.Position{Line: 1, Column: 9, Offset: 9}, + }, + }, + FunctionBlock: &ast.FunctionBlock{ + Block: &ast.Block{ + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 11, Offset: 11}, + EndPos: ast.Position{Line: 1, Column: 13, Offset: 13}, + }, + }, + }, + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + result, + ) + }) + + t.Run("without return type, pub", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseDeclarations("pub fun foo () { }") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.FunctionDeclaration{ + Access: ast.AccessAll, + Identifier: ast.Identifier{ + Identifier: "foo", + Pos: ast.Position{Line: 1, Column: 8, Offset: 8}, + }, + ParameterList: &ast.ParameterList{ + Parameters: nil, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 12, Offset: 12}, + EndPos: ast.Position{Line: 1, Column: 13, Offset: 13}, + }, + }, + FunctionBlock: &ast.FunctionBlock{ + Block: &ast.Block{ + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 15, Offset: 15}, + EndPos: ast.Position{Line: 1, Column: 17, Offset: 17}, + }, + }, + }, + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + result, + ) + }) + + t.Run("with return type", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseDeclarations("fun foo (): X { }") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.FunctionDeclaration{ + Access: ast.AccessNotSpecified, + Identifier: ast.Identifier{ + Identifier: "foo", + Pos: ast.Position{Line: 1, Column: 4, Offset: 4}, + }, + ParameterList: &ast.ParameterList{ + Parameters: nil, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 8, Offset: 8}, + EndPos: ast.Position{Line: 1, Column: 9, Offset: 9}, + }, + }, + ReturnTypeAnnotation: &ast.TypeAnnotation{ + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "X", + Pos: ast.Position{Line: 1, Column: 12, Offset: 12}, + }, + }, + StartPos: ast.Position{Line: 1, Column: 12, Offset: 12}, + }, + FunctionBlock: &ast.FunctionBlock{ + Block: &ast.Block{ + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 14, Offset: 14}, + EndPos: ast.Position{Line: 1, Column: 16, Offset: 16}, + }, + }, + }, + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + result, + ) + }) + + t.Run("without return type, with pre and post conditions", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseStatements(` + fun foo () { + pre { + true : "test" + 2 > 1 : "foo" + } + + post { + false + } + + bar() + } + `) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Statement{ + &ast.FunctionDeclaration{ + Access: ast.AccessNotSpecified, + Identifier: ast.Identifier{ + Identifier: "foo", + Pos: ast.Position{Line: 2, Column: 14, Offset: 15}, + }, + ParameterList: &ast.ParameterList{ + Parameters: nil, + Range: ast.Range{ + StartPos: ast.Position{Line: 2, Column: 18, Offset: 19}, + EndPos: ast.Position{Line: 2, Column: 19, Offset: 20}, + }, + }, + FunctionBlock: &ast.FunctionBlock{ + PreConditions: &ast.Conditions{ + &ast.TestCondition{ + Test: &ast.BoolExpression{ + Value: true, + Range: ast.Range{ + StartPos: ast.Position{Line: 4, Column: 17, Offset: 61}, + EndPos: ast.Position{Line: 4, Column: 20, Offset: 64}, + }, + }, + Message: &ast.StringExpression{ + Value: "test", + Range: ast.Range{ + StartPos: ast.Position{Line: 4, Column: 24, Offset: 68}, + EndPos: ast.Position{Line: 4, Column: 29, Offset: 73}, + }, + }, + }, + &ast.TestCondition{ + Test: &ast.BinaryExpression{ + Operation: ast.OperationGreater, + Left: &ast.IntegerExpression{ + PositiveLiteral: []byte("2"), + Value: big.NewInt(2), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 5, Column: 17, Offset: 92}, + EndPos: ast.Position{Line: 5, Column: 17, Offset: 92}, + }, + }, + Right: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 5, Column: 21, Offset: 96}, + EndPos: ast.Position{Line: 5, Column: 21, Offset: 96}, + }, + }, + }, + Message: &ast.StringExpression{ + Value: "foo", + Range: ast.Range{ + StartPos: ast.Position{Line: 5, Column: 25, Offset: 100}, + EndPos: ast.Position{Line: 5, Column: 29, Offset: 104}, + }, + }, + }, + }, + PostConditions: &ast.Conditions{ + &ast.TestCondition{ + Test: &ast.BoolExpression{ + Value: false, + Range: ast.Range{ + StartPos: ast.Position{Line: 9, Column: 17, Offset: 161}, + EndPos: ast.Position{Line: 9, Column: 21, Offset: 165}, + }, + }, + Message: nil, + }, + }, + Block: &ast.Block{ + Statements: []ast.Statement{ + &ast.ExpressionStatement{ + Expression: &ast.InvocationExpression{ + InvokedExpression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "bar", + Pos: ast.Position{Line: 12, Column: 14, Offset: 198}, + }, + }, + ArgumentsStartPos: ast.Position{Line: 12, Column: 17, Offset: 201}, + EndPos: ast.Position{Line: 12, Column: 18, Offset: 202}, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Line: 2, Column: 21, Offset: 22}, + EndPos: ast.Position{Line: 13, Column: 10, Offset: 214}, + }, + }, + }, + StartPos: ast.Position{Line: 2, Column: 10, Offset: 11}, + }, + }, + result, + ) + }) + + t.Run("with docstring, single line comment", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseDeclarations("/// Test\nfun foo() {}") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.FunctionDeclaration{ + Access: ast.AccessNotSpecified, + Identifier: ast.Identifier{ + Identifier: "foo", + Pos: ast.Position{Line: 2, Column: 4, Offset: 13}, + }, + ParameterList: &ast.ParameterList{ + Parameters: nil, + Range: ast.Range{ + StartPos: ast.Position{Line: 2, Column: 7, Offset: 16}, + EndPos: ast.Position{Line: 2, Column: 8, Offset: 17}, + }, + }, + FunctionBlock: &ast.FunctionBlock{ + Block: &ast.Block{ + Range: ast.Range{ + StartPos: ast.Position{Line: 2, Column: 10, Offset: 19}, + EndPos: ast.Position{Line: 2, Column: 11, Offset: 20}, + }, + }, + }, + DocString: " Test", + StartPos: ast.Position{Line: 2, Column: 0, Offset: 9}, + }, + }, + result, + ) + }) + + t.Run("with docstring, two line comments", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseDeclarations("\n /// First line\n \n/// Second line\n\n\nfun foo() {}") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.FunctionDeclaration{ + Access: ast.AccessNotSpecified, + Identifier: ast.Identifier{ + Identifier: "foo", + Pos: ast.Position{Line: 7, Column: 4, Offset: 43}, + }, + ParameterList: &ast.ParameterList{ + Parameters: nil, + Range: ast.Range{ + StartPos: ast.Position{Line: 7, Column: 7, Offset: 46}, + EndPos: ast.Position{Line: 7, Column: 8, Offset: 47}, + }, + }, + FunctionBlock: &ast.FunctionBlock{ + Block: &ast.Block{ + Range: ast.Range{ + StartPos: ast.Position{Line: 7, Column: 10, Offset: 49}, + EndPos: ast.Position{Line: 7, Column: 11, Offset: 50}, + }, + }, + }, + DocString: " First line\n Second line", + StartPos: ast.Position{Line: 7, Column: 0, Offset: 39}, + }, + }, + result, + ) + }) + + t.Run("with docstring, block comment", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseDeclarations("\n /** Cool dogs.\n\n Cool cats!! */\n\n\nfun foo() {}") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.FunctionDeclaration{ + Access: ast.AccessNotSpecified, + Identifier: ast.Identifier{ + Identifier: "foo", + Pos: ast.Position{Line: 7, Column: 4, Offset: 43}, + }, + ParameterList: &ast.ParameterList{ + Parameters: nil, + Range: ast.Range{ + StartPos: ast.Position{Line: 7, Column: 7, Offset: 46}, + EndPos: ast.Position{Line: 7, Column: 8, Offset: 47}, + }, + }, + FunctionBlock: &ast.FunctionBlock{ + Block: &ast.Block{ + Range: ast.Range{ + StartPos: ast.Position{Line: 7, Column: 10, Offset: 49}, + EndPos: ast.Position{Line: 7, Column: 11, Offset: 50}, + }, + }, + }, + DocString: " Cool dogs.\n\n Cool cats!! ", + StartPos: ast.Position{Line: 7, Column: 0, Offset: 39}, + }, + }, + result, + ) + }) + + t.Run("without space after return type", func(t *testing.T) { + + // A brace after the return type is ambiguous: + // It could be the start of a restricted type. + // However, if there is space after the brace, which is most common + // in function declarations, we consider it not a restricted type + + t.Parallel() + + result, errs := testParseDeclarations("fun main(): Int{ return 1 }") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.FunctionDeclaration{ + Access: ast.AccessNotSpecified, + Identifier: ast.Identifier{ + Identifier: "main", + Pos: ast.Position{Line: 1, Column: 4, Offset: 4}, + }, + ParameterList: &ast.ParameterList{ + Parameters: nil, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 8, Offset: 8}, + EndPos: ast.Position{Line: 1, Column: 9, Offset: 9}, + }, + }, + ReturnTypeAnnotation: &ast.TypeAnnotation{ + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Int", + Pos: ast.Position{Line: 1, Column: 12, Offset: 12}, + }, + }, + StartPos: ast.Position{Line: 1, Column: 12, Offset: 12}, + }, + FunctionBlock: &ast.FunctionBlock{ + Block: &ast.Block{ + Statements: []ast.Statement{ + &ast.ReturnStatement{ + Expression: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 24, Offset: 24}, + EndPos: ast.Position{Line: 1, Column: 24, Offset: 24}, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 17, Offset: 17}, + EndPos: ast.Position{Line: 1, Column: 24, Offset: 24}, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 15, Offset: 15}, + EndPos: ast.Position{Line: 1, Column: 26, Offset: 26}, + }, + }, + }, + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + result, + ) + }) + + t.Run("native, enabled", func(t *testing.T) { + + t.Parallel() + + result, errs := ParseDeclarations( + nil, + []byte("native fun foo() {}"), + Config{ + NativeModifierEnabled: true, + }, + ) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.FunctionDeclaration{ + Access: ast.AccessNotSpecified, + Flags: ast.FunctionDeclarationFlagsIsNative, + Identifier: ast.Identifier{ + Identifier: "foo", + Pos: ast.Position{Line: 1, Column: 11, Offset: 11}, + }, + ParameterList: &ast.ParameterList{ + Parameters: nil, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 14, Offset: 14}, + EndPos: ast.Position{Line: 1, Column: 15, Offset: 15}, + }, + }, + FunctionBlock: &ast.FunctionBlock{ + Block: &ast.Block{ + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 17, Offset: 17}, + EndPos: ast.Position{Line: 1, Column: 18, Offset: 18}, + }, + }, + }, + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + result, + ) + }) + + t.Run("native, disabled", func(t *testing.T) { + + t.Parallel() + + _, errs := testParseDeclarations("native fun foo() {}") + + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "unexpected token: identifier", + Pos: ast.Position{Offset: 0, Line: 1, Column: 0}, + }, + }, + errs, + ) + }) + + t.Run("static", func(t *testing.T) { + + t.Parallel() + + result, errs := ParseDeclarations( + nil, + []byte("static fun foo() {}"), + Config{ + StaticModifierEnabled: true, + }, + ) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.FunctionDeclaration{ + Access: ast.AccessNotSpecified, + Flags: ast.FunctionDeclarationFlagsIsStatic, + Identifier: ast.Identifier{ + Identifier: "foo", + Pos: ast.Position{Line: 1, Column: 11, Offset: 11}, + }, + ParameterList: &ast.ParameterList{ + Parameters: nil, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 14, Offset: 14}, + EndPos: ast.Position{Line: 1, Column: 15, Offset: 15}, + }, + }, + FunctionBlock: &ast.FunctionBlock{ + Block: &ast.Block{ + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 17, Offset: 17}, + EndPos: ast.Position{Line: 1, Column: 18, Offset: 18}, + }, + }, + }, + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + result, + ) + }) + + t.Run("static, disabled", func(t *testing.T) { + + t.Parallel() + + _, errs := testParseDeclarations("static fun foo() {}") + + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "unexpected token: identifier", + Pos: ast.Position{Offset: 0, Line: 1, Column: 0}, + }, + }, + errs, + ) + }) + + t.Run("static native, enabled", func(t *testing.T) { + + t.Parallel() + + result, errs := ParseDeclarations( + nil, + []byte("static native fun foo() {}"), + Config{ + StaticModifierEnabled: true, + NativeModifierEnabled: true, + }, + ) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.FunctionDeclaration{ + Access: ast.AccessNotSpecified, + Flags: ast.FunctionDeclarationFlagsIsStatic | ast.FunctionDeclarationFlagsIsNative, + Identifier: ast.Identifier{ + Identifier: "foo", + Pos: ast.Position{Line: 1, Column: 18, Offset: 18}, + }, + ParameterList: &ast.ParameterList{ + Parameters: nil, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 21, Offset: 21}, + EndPos: ast.Position{Line: 1, Column: 22, Offset: 22}, + }, + }, + FunctionBlock: &ast.FunctionBlock{ + Block: &ast.Block{ + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 24, Offset: 24}, + EndPos: ast.Position{Line: 1, Column: 25, Offset: 25}, + }, + }, + }, + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + result, + ) + }) + + t.Run("static native, disabled", func(t *testing.T) { + + t.Parallel() + + _, errs := testParseDeclarations("static native fun foo() {}") + + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "unexpected token: identifier", + Pos: ast.Position{Offset: 0, Line: 1, Column: 0}, + }, + }, + errs, + ) + }) + + t.Run("native static, enabled", func(t *testing.T) { + + t.Parallel() + + _, errs := ParseDeclarations( + nil, + []byte("native static fun foo() {}"), + Config{ + StaticModifierEnabled: true, + NativeModifierEnabled: true, + }, + ) + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "invalid static modifier after native modifier", + Pos: ast.Position{Offset: 7, Line: 1, Column: 7}, + }, + }, + errs, + ) + }) + + t.Run("native static, disabled", func(t *testing.T) { + + t.Parallel() + + _, errs := testParseDeclarations("native static fun foo() {}") + + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "unexpected token: identifier", + Pos: ast.Position{Offset: 0, Line: 1, Column: 0}, + }, + }, + errs, + ) + }) + + t.Run("pub static native, enabled", func(t *testing.T) { + + t.Parallel() + + result, errs := ParseDeclarations( + nil, + []byte("pub static native fun foo() {}"), + Config{ + StaticModifierEnabled: true, + NativeModifierEnabled: true, + }, + ) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.FunctionDeclaration{ + Access: ast.AccessAll, + Flags: ast.FunctionDeclarationFlagsIsStatic | ast.FunctionDeclarationFlagsIsNative, + Identifier: ast.Identifier{ + Identifier: "foo", + Pos: ast.Position{Line: 1, Column: 22, Offset: 22}, + }, + ParameterList: &ast.ParameterList{ + Parameters: nil, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 25, Offset: 25}, + EndPos: ast.Position{Line: 1, Column: 26, Offset: 26}, + }, + }, + FunctionBlock: &ast.FunctionBlock{ + Block: &ast.Block{ + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 28, Offset: 28}, + EndPos: ast.Position{Line: 1, Column: 29, Offset: 29}, + }, + }, + }, + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + result, + ) + }) + + t.Run("pub static native, disabled", func(t *testing.T) { + + t.Parallel() + + _, errs := testParseDeclarations("pub static native fun foo() {}") + + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "unexpected token: identifier", + Pos: ast.Position{Offset: 4, Line: 1, Column: 4}, + }, + }, + errs, + ) + }) + + t.Run("with empty type parameters, enabled", func(t *testing.T) { + + t.Parallel() + + result, errs := ParseDeclarations( + nil, + []byte("fun foo < > () {}"), + Config{ + TypeParametersEnabled: true, + }, + ) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.FunctionDeclaration{ + Access: ast.AccessNotSpecified, + Identifier: ast.Identifier{ + Identifier: "foo", + Pos: ast.Position{Line: 1, Column: 4, Offset: 4}, + }, + TypeParameterList: &ast.TypeParameterList{ + TypeParameters: nil, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 9, Offset: 9}, + EndPos: ast.Position{Line: 1, Column: 11, Offset: 11}, + }, + }, + ParameterList: &ast.ParameterList{ + Parameters: nil, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 13, Offset: 13}, + EndPos: ast.Position{Line: 1, Column: 14, Offset: 14}, + }, + }, + FunctionBlock: &ast.FunctionBlock{ + Block: &ast.Block{ + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 16, Offset: 16}, + EndPos: ast.Position{Line: 1, Column: 17, Offset: 17}, + }, + }, + }, + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + result, + ) + }) + + t.Run("with type parameters, single type parameter, enabled", func(t *testing.T) { + + t.Parallel() + + result, errs := ParseDeclarations( + nil, + []byte("fun foo < A > () {}"), + Config{ + TypeParametersEnabled: true, + }, + ) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.FunctionDeclaration{ + Access: ast.AccessNotSpecified, + Identifier: ast.Identifier{ + Identifier: "foo", + Pos: ast.Position{Line: 1, Column: 4, Offset: 4}, + }, + TypeParameterList: &ast.TypeParameterList{ + TypeParameters: []*ast.TypeParameter{ + { + Identifier: ast.Identifier{ + Identifier: "A", + Pos: ast.Position{Offset: 11, Line: 1, Column: 11}, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 9, Offset: 9}, + EndPos: ast.Position{Line: 1, Column: 14, Offset: 14}, + }, + }, + ParameterList: &ast.ParameterList{ + Parameters: nil, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 16, Offset: 16}, + EndPos: ast.Position{Line: 1, Column: 17, Offset: 17}, + }, + }, + FunctionBlock: &ast.FunctionBlock{ + Block: &ast.Block{ + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 19, Offset: 19}, + EndPos: ast.Position{Line: 1, Column: 20, Offset: 20}, + }, + }, + }, + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + result, + ) + }) + + t.Run("with type parameters, multiple parameters, type bound, enabled", func(t *testing.T) { + + t.Parallel() + + result, errs := ParseDeclarations( + nil, + []byte("fun foo < A , B : C > () {}"), + Config{ + TypeParametersEnabled: true, + }, + ) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.FunctionDeclaration{ + Access: ast.AccessNotSpecified, + Identifier: ast.Identifier{ + Identifier: "foo", + Pos: ast.Position{Line: 1, Column: 4, Offset: 4}, + }, + TypeParameterList: &ast.TypeParameterList{ + TypeParameters: []*ast.TypeParameter{ + { + Identifier: ast.Identifier{ + Identifier: "A", + Pos: ast.Position{Offset: 11, Line: 1, Column: 11}, + }, + }, + { + Identifier: ast.Identifier{ + Identifier: "B", + Pos: ast.Position{Offset: 16, Line: 1, Column: 16}, + }, + TypeBound: &ast.TypeAnnotation{ + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "C", + Pos: ast.Position{Offset: 20, Line: 1, Column: 20}, + }, + }, + StartPos: ast.Position{Offset: 20, Line: 1, Column: 20}, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 9, Offset: 9}, + EndPos: ast.Position{Line: 1, Column: 22, Offset: 22}, + }, + }, + ParameterList: &ast.ParameterList{ + Parameters: nil, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 24, Offset: 24}, + EndPos: ast.Position{Line: 1, Column: 25, Offset: 25}, + }, + }, + FunctionBlock: &ast.FunctionBlock{ + Block: &ast.Block{ + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 27, Offset: 27}, + EndPos: ast.Position{Line: 1, Column: 28, Offset: 28}, + }, + }, + }, + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + result, + ) + }) + + t.Run("with type parameters, disabled", func(t *testing.T) { + + t.Parallel() + + _, errs := testParseDeclarations("fun foo() {}") + + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "expected '(' as start of parameter list, got '<'", + Pos: ast.Position{Offset: 7, Line: 1, Column: 7}, + }, + }, + errs, + ) + }) + + t.Run("missing type parameter list end, enabled", func(t *testing.T) { + + t.Parallel() + + _, errs := ParseDeclarations( + nil, + []byte("fun foo < "), + Config{ + TypeParametersEnabled: true, + }, + ) + + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "missing '>' at end of type parameter list", + Pos: ast.Position{Offset: 11, Line: 1, Column: 11}, + }, + }, + errs, + ) + }) + + t.Run("missing type parameter list separator, enabled", func(t *testing.T) { + + t.Parallel() + + _, errs := ParseDeclarations( + nil, + []byte("fun foo < A B > () { } "), + Config{ + TypeParametersEnabled: true, + }, + ) + + utils.AssertEqualWithDiff(t, + []error{ + &MissingCommaInParameterListError{ + Pos: ast.Position{Offset: 13, Line: 1, Column: 13}, + }, + }, + errs, + ) + }) + +} + +func TestParseAccess(t *testing.T) { + + t.Parallel() + + parse := func(input string) (ast.Access, []error) { + return Parse( + nil, + []byte(input), + parseAccess, + Config{}, + ) + } + + t.Run("pub", func(t *testing.T) { + + t.Parallel() + + result, errs := parse("pub") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + ast.AccessAll, + result, + ) + }) + + t.Run("pub(set)", func(t *testing.T) { + + t.Parallel() + + result, errs := parse("pub ( set )") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + ast.PubSettableLegacy, + result, + ) + }) + + t.Run("pub, missing set keyword", func(t *testing.T) { + + t.Parallel() + + result, errs := parse("pub ( ") + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "expected keyword \"set\", got EOF", + Pos: ast.Position{Offset: 6, Line: 1, Column: 6}, + }, + }, + errs, + ) + + utils.AssertEqualWithDiff(t, + ast.AccessNotSpecified, + result, + ) + }) + + t.Run("pub, missing closing paren", func(t *testing.T) { + + t.Parallel() + + result, errs := parse("pub ( set ") + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "expected token ')'", + Pos: ast.Position{Offset: 10, Line: 1, Column: 10}, + }, + }, + errs, + ) + + utils.AssertEqualWithDiff(t, + ast.AccessNotSpecified, + result, + ) + }) + + t.Run("pub, invalid inner keyword", func(t *testing.T) { + + t.Parallel() + + result, errs := parse("pub ( foo )") + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "expected keyword \"set\", got \"foo\"", + Pos: ast.Position{Offset: 6, Line: 1, Column: 6}, + }, + }, + errs, + ) + + utils.AssertEqualWithDiff(t, + ast.AccessNotSpecified, + result, + ) + }) + + t.Run("priv", func(t *testing.T) { + + t.Parallel() + + result, errs := parse("priv") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + ast.AccessSelf, + result, + ) + }) + + t.Run("access(all)", func(t *testing.T) { + + t.Parallel() + + result, errs := parse("access ( all )") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + ast.AccessAll, + result, + ) + }) + + t.Run("access(account)", func(t *testing.T) { + + t.Parallel() + + result, errs := parse("access ( account )") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + ast.AccessAccount, + result, + ) + }) + + t.Run("access(contract)", func(t *testing.T) { + + t.Parallel() + + result, errs := parse("access ( contract )") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + ast.AccessContract, + result, + ) + }) + + t.Run("access(self)", func(t *testing.T) { + + t.Parallel() + + result, errs := parse("access ( self )") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + ast.AccessSelf, + result, + ) + }) + + t.Run("access, missing keyword", func(t *testing.T) { + + t.Parallel() + + result, errs := parse("access ( ") + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "expected keyword \"all\", \"account\", \"contract\", or \"self\", got EOF", + Pos: ast.Position{Offset: 9, Line: 1, Column: 9}, + }, + }, + errs, + ) + + utils.AssertEqualWithDiff(t, + ast.AccessNotSpecified, + result, + ) + }) + + t.Run("access, missing closing paren", func(t *testing.T) { + + t.Parallel() + + result, errs := parse("access ( self ") + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "expected token ')'", + Pos: ast.Position{Offset: 14, Line: 1, Column: 14}, + }, + }, + errs, + ) + + utils.AssertEqualWithDiff(t, + ast.AccessNotSpecified, + result, + ) + }) + + t.Run("access, invalid inner keyword", func(t *testing.T) { + + t.Parallel() + + result, errs := parse("access ( foo )") + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "expected keyword \"all\", \"account\", \"contract\", or \"self\", got \"foo\"", + Pos: ast.Position{Offset: 9, Line: 1, Column: 9}, + }, + }, + errs, + ) + + utils.AssertEqualWithDiff(t, + ast.AccessNotSpecified, + result, + ) + }) +} + +func TestParseImportDeclaration(t *testing.T) { + + t.Parallel() + + t.Run("no identifiers, missing location", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseDeclarations(` import`) + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "unexpected end in import declaration: expected string, address, or identifier", + Pos: ast.Position{Offset: 7, Line: 1, Column: 7}, + }, + }, + errs, + ) + + var expected []ast.Declaration + + utils.AssertEqualWithDiff(t, + expected, + result, + ) + }) + + t.Run("no identifiers, string location", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseDeclarations(` import "foo"`) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.ImportDeclaration{ + Identifiers: nil, + Location: common.StringLocation("foo"), + LocationPos: ast.Position{Line: 1, Column: 8, Offset: 8}, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + EndPos: ast.Position{Line: 1, Column: 12, Offset: 12}, + }, + }, + }, + result, + ) + }) + + t.Run("no identifiers, address location", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseDeclarations(` import 0x42`) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.ImportDeclaration{ + Identifiers: nil, + Location: common.AddressLocation{ + Address: common.MustBytesToAddress([]byte{0x42}), + }, + LocationPos: ast.Position{Line: 1, Column: 8, Offset: 8}, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + EndPos: ast.Position{Line: 1, Column: 11, Offset: 11}, + }, + }, + }, + result, + ) + }) + + t.Run("no identifiers, address location, address too long", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseDeclarations(` import 0x10000000000000001`) + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "address too large", + Pos: ast.Position{Offset: 8, Line: 1, Column: 8}, + }, + }, + errs, + ) + + expected := []ast.Declaration{ + &ast.ImportDeclaration{ + Identifiers: nil, + Location: common.AddressLocation{ + Address: common.MustBytesToAddress([]byte{0x0}), + }, + LocationPos: ast.Position{Line: 1, Column: 8, Offset: 8}, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + EndPos: ast.Position{Line: 1, Column: 26, Offset: 26}, + }, + }, + } + + utils.AssertEqualWithDiff(t, + expected, + result, + ) + }) + + t.Run("no identifiers, integer location", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseDeclarations(` import 1`) + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "unexpected token in import declaration: " + + "got decimal integer, expected string, address, or identifier", + Pos: ast.Position{Offset: 8, Line: 1, Column: 8}, + }, + }, + errs, + ) + + var expected []ast.Declaration + + utils.AssertEqualWithDiff(t, + expected, + result, + ) + + }) + + t.Run("one identifier, string location", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseDeclarations(` import foo from "bar"`) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.ImportDeclaration{ + Identifiers: []ast.Identifier{ + { + Identifier: "foo", + Pos: ast.Position{Line: 1, Column: 8, Offset: 8}, + }, + }, + Location: common.StringLocation("bar"), + LocationPos: ast.Position{Line: 1, Column: 17, Offset: 17}, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + EndPos: ast.Position{Line: 1, Column: 21, Offset: 21}, + }, + }, + }, + result, + ) + }) + + t.Run("one identifier, string location, missing from keyword", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseDeclarations(` import foo "bar"`) + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "unexpected token in import declaration: " + + "got string, expected keyword \"from\" or ','", + Pos: ast.Position{Offset: 12, Line: 1, Column: 12}, + }, + }, + errs, + ) + + var expected []ast.Declaration + + utils.AssertEqualWithDiff(t, + expected, + result, + ) + }) + + t.Run("three identifiers, address location", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseDeclarations(` import foo , bar , baz from 0x42`) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.ImportDeclaration{ + Identifiers: []ast.Identifier{ + { + Identifier: "foo", + Pos: ast.Position{Line: 1, Column: 8, Offset: 8}, + }, + { + Identifier: "bar", + Pos: ast.Position{Line: 1, Column: 14, Offset: 14}, + }, + { + Identifier: "baz", + Pos: ast.Position{Line: 1, Column: 20, Offset: 20}, + }, + }, + Location: common.AddressLocation{ + Address: common.MustBytesToAddress([]byte{0x42}), + }, + LocationPos: ast.Position{Line: 1, Column: 29, Offset: 29}, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + EndPos: ast.Position{Line: 1, Column: 32, Offset: 32}, + }, + }, + }, + result, + ) + }) + + t.Run("two identifiers, address location, extra comma", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseDeclarations(` import foo , bar , from 0x42`) + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: `expected identifier, got keyword "from"`, + Pos: ast.Position{Offset: 20, Line: 1, Column: 20}, + }, + }, + errs, + ) + + var expected []ast.Declaration + + utils.AssertEqualWithDiff(t, + expected, + result, + ) + }) + + t.Run("no identifiers, identifier location", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseDeclarations(` import foo`) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.ImportDeclaration{ + Identifiers: nil, + Location: common.IdentifierLocation("foo"), + LocationPos: ast.Position{Line: 1, Column: 8, Offset: 8}, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + EndPos: ast.Position{Line: 1, Column: 10, Offset: 10}, + }, + }, + }, + result, + ) + }) + + t.Run("from keyword as second identifier", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseDeclarations(` + import foo, from from 0x42 + import foo, from, bar from 0x42 + `) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.ImportDeclaration{ + Identifiers: []ast.Identifier{ + { + Identifier: "foo", + Pos: ast.Position{Line: 2, Column: 10, Offset: 11}, + }, + { + Identifier: "from", + Pos: ast.Position{Line: 2, Column: 15, Offset: 16}, + }, + }, + Location: common.AddressLocation{ + Address: common.MustBytesToAddress([]byte{0x42}), + }, + LocationPos: ast.Position{Line: 2, Column: 25, Offset: 26}, + Range: ast.Range{ + StartPos: ast.Position{Line: 2, Column: 3, Offset: 4}, + EndPos: ast.Position{Line: 2, Column: 28, Offset: 29}, + }, + }, + &ast.ImportDeclaration{ + Identifiers: []ast.Identifier{ + { + Identifier: "foo", + Pos: ast.Position{Line: 3, Column: 10, Offset: 41}, + }, + { + Identifier: "from", + Pos: ast.Position{Line: 3, Column: 15, Offset: 46}, + }, + { + Identifier: "bar", + Pos: ast.Position{Line: 3, Column: 21, Offset: 52}, + }, + }, + Location: common.AddressLocation{ + Address: common.MustBytesToAddress([]byte{0x42}), + }, + LocationPos: ast.Position{Line: 3, Column: 30, Offset: 61}, + Range: ast.Range{ + StartPos: ast.Position{Line: 3, Column: 3, Offset: 34}, + EndPos: ast.Position{Line: 3, Column: 33, Offset: 64}, + }, + }, + }, + result, + ) + }) +} + +func TestParseEvent(t *testing.T) { + + t.Parallel() + + t.Run("no parameters", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseDeclarations("event E()") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.CompositeDeclaration{ + Access: ast.AccessNotSpecified, + CompositeKind: common.CompositeKindEvent, + Identifier: ast.Identifier{ + Identifier: "E", + Pos: ast.Position{Offset: 6, Line: 1, Column: 6}, + }, + Members: ast.NewUnmeteredMembers( + []ast.Declaration{ + &ast.SpecialFunctionDeclaration{ + Kind: common.DeclarationKindInitializer, + FunctionDeclaration: &ast.FunctionDeclaration{ + Access: ast.AccessNotSpecified, + ParameterList: &ast.ParameterList{ + Range: ast.Range{ + StartPos: ast.Position{Offset: 7, Line: 1, Column: 7}, + EndPos: ast.Position{Offset: 8, Line: 1, Column: 8}, + }, + }, + StartPos: ast.Position{Offset: 7, Line: 1, Column: 7}, + }, + }, + }, + ), + Range: ast.Range{ + StartPos: ast.Position{Offset: 0, Line: 1, Column: 0}, + EndPos: ast.Position{Offset: 8, Line: 1, Column: 8}, + }, + }, + }, + result, + ) + }) + + t.Run("two parameters, private", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseDeclarations(" priv event E2 ( a : Int , b : String )") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + + &ast.CompositeDeclaration{ + Access: ast.AccessSelf, + CompositeKind: common.CompositeKindEvent, + Identifier: ast.Identifier{ + Identifier: "E2", + Pos: ast.Position{Offset: 12, Line: 1, Column: 12}, + }, + Members: ast.NewUnmeteredMembers( + []ast.Declaration{ + &ast.SpecialFunctionDeclaration{ + Kind: common.DeclarationKindInitializer, + FunctionDeclaration: &ast.FunctionDeclaration{ + Access: ast.AccessNotSpecified, + ParameterList: &ast.ParameterList{ + Parameters: []*ast.Parameter{ + { + Label: "", + Identifier: ast.Identifier{ + Identifier: "a", + Pos: ast.Position{Offset: 17, Line: 1, Column: 17}, + }, + TypeAnnotation: &ast.TypeAnnotation{ + IsResource: false, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Int", + Pos: ast.Position{Offset: 21, Line: 1, Column: 21}, + }, + }, + StartPos: ast.Position{Offset: 21, Line: 1, Column: 21}, + }, + StartPos: ast.Position{Offset: 17, Line: 1, Column: 17}, + }, + { + Label: "", + Identifier: ast.Identifier{ + Identifier: "b", + Pos: ast.Position{Offset: 27, Line: 1, Column: 27}, + }, + TypeAnnotation: &ast.TypeAnnotation{ + IsResource: false, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "String", + Pos: ast.Position{Offset: 31, Line: 1, Column: 31}, + }, + }, + StartPos: ast.Position{Offset: 31, Line: 1, Column: 31}, + }, + StartPos: ast.Position{Offset: 27, Line: 1, Column: 27}, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 15, Line: 1, Column: 15}, + EndPos: ast.Position{Offset: 38, Line: 1, Column: 38}, + }, + }, + StartPos: ast.Position{Offset: 15, Line: 1, Column: 15}, + }, + }, + }, + ), + Range: ast.Range{ + StartPos: ast.Position{Offset: 1, Line: 1, Column: 1}, + EndPos: ast.Position{Offset: 38, Line: 1, Column: 38}, + }, + }, + }, + result, + ) + }) +} + +func TestParseFieldWithVariableKind(t *testing.T) { + + t.Parallel() + + parse := func(input string) (*ast.FieldDeclaration, []error) { + return Parse( + nil, + []byte(input), + func(p *parser) (*ast.FieldDeclaration, error) { + return parseFieldWithVariableKind( + p, + ast.AccessNotSpecified, + nil, + nil, + nil, + "", + ) + }, + Config{}, + ) + } + + t.Run("variable", func(t *testing.T) { + + t.Parallel() + + result, errs := parse("var x : Int") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.FieldDeclaration{ + Access: ast.AccessNotSpecified, + VariableKind: ast.VariableKindVariable, + Identifier: ast.Identifier{ + Identifier: "x", + Pos: ast.Position{Line: 1, Column: 4, Offset: 4}, + }, + TypeAnnotation: &ast.TypeAnnotation{ + IsResource: false, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Int", + Pos: ast.Position{Line: 1, Column: 8, Offset: 8}, + }, + }, + StartPos: ast.Position{Line: 1, Column: 8, Offset: 8}, + }, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 10, Offset: 10}, + }, + }, + result, + ) + }) + + t.Run("constant", func(t *testing.T) { + + t.Parallel() + + result, errs := parse("let x : Int") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.FieldDeclaration{ + Access: ast.AccessNotSpecified, + VariableKind: ast.VariableKindConstant, + Identifier: ast.Identifier{ + Identifier: "x", + Pos: ast.Position{Line: 1, Column: 4, Offset: 4}, + }, + TypeAnnotation: &ast.TypeAnnotation{ + IsResource: false, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Int", + Pos: ast.Position{Line: 1, Column: 8, Offset: 8}, + }, + }, + StartPos: ast.Position{Line: 1, Column: 8, Offset: 8}, + }, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 10, Offset: 10}, + }, + }, + result, + ) + }) +} + +func TestParseField(t *testing.T) { + + t.Parallel() + + parse := func(input string, config Config) (ast.Declaration, []error) { + return Parse( + nil, + []byte(input), + func(p *parser) (ast.Declaration, error) { + return parseMemberOrNestedDeclaration( + p, + "", + ) + }, + config, + ) + } + + t.Run("native, enabled", func(t *testing.T) { + + t.Parallel() + + result, errs := parse( + "native let foo: Int", + Config{ + NativeModifierEnabled: true, + }, + ) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.FieldDeclaration{ + Access: ast.AccessNotSpecified, + Flags: ast.FieldDeclarationFlagsIsNative, + VariableKind: ast.VariableKindConstant, + Identifier: ast.Identifier{ + Identifier: "foo", + Pos: ast.Position{Line: 1, Column: 11, Offset: 11}, + }, + TypeAnnotation: &ast.TypeAnnotation{ + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Int", + Pos: ast.Position{Line: 1, Column: 16, Offset: 16}, + }, + }, + StartPos: ast.Position{Line: 1, Column: 16, Offset: 16}, + }, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 18, Offset: 18}, + }, + }, + result, + ) + }) + + t.Run("native, disabled", func(t *testing.T) { + + t.Parallel() + + _, errs := parse("native let foo: Int", Config{}) + + // For now, leading unknown identifiers are valid. + // This will be rejected in Stable Cadence. + + require.Empty(t, errs) + }) + + t.Run("static", func(t *testing.T) { + + t.Parallel() + + result, errs := parse( + "static let foo: Int", + Config{ + StaticModifierEnabled: true, + }, + ) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.FieldDeclaration{ + Access: ast.AccessNotSpecified, + Flags: ast.FieldDeclarationFlagsIsStatic, + VariableKind: ast.VariableKindConstant, + Identifier: ast.Identifier{ + Identifier: "foo", + Pos: ast.Position{Line: 1, Column: 11, Offset: 11}, + }, + TypeAnnotation: &ast.TypeAnnotation{ + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Int", + Pos: ast.Position{Line: 1, Column: 16, Offset: 16}, + }, + }, + StartPos: ast.Position{Line: 1, Column: 16, Offset: 16}, + }, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 18, Offset: 18}, + }, + }, + result, + ) + }) + + t.Run("static, disabled", func(t *testing.T) { + + t.Parallel() + + _, errs := parse( + "static let foo: Int", + Config{}, + ) + + // For now, leading unknown identifiers are valid. + // This will be rejected in Stable Cadence. + + require.Empty(t, errs) + }) + + t.Run("static native, enabled", func(t *testing.T) { + + t.Parallel() + + result, errs := parse( + "static native let foo: Int", + Config{ + StaticModifierEnabled: true, + NativeModifierEnabled: true, + }, + ) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.FieldDeclaration{ + Access: ast.AccessNotSpecified, + Flags: ast.FieldDeclarationFlagsIsStatic | ast.FieldDeclarationFlagsIsNative, + VariableKind: ast.VariableKindConstant, + Identifier: ast.Identifier{ + Identifier: "foo", + Pos: ast.Position{Line: 1, Column: 18, Offset: 18}, + }, + TypeAnnotation: &ast.TypeAnnotation{ + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Int", + Pos: ast.Position{Line: 1, Column: 23, Offset: 23}, + }, + }, + StartPos: ast.Position{Line: 1, Column: 23, Offset: 23}, + }, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 25, Offset: 25}, + }, + }, + result, + ) + }) + + t.Run("static native, disabled", func(t *testing.T) { + + t.Parallel() + + _, errs := parse("static native let foo: Int", Config{}) + + // For now, leading unknown identifiers are valid. + // This will be rejected in Stable Cadence. + + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "unexpected identifier", + Pos: ast.Position{Offset: 7, Line: 1, Column: 7}, + }, + }, + errs, + ) + }) + + t.Run("native static, enabled", func(t *testing.T) { + + t.Parallel() + + _, errs := parse( + "native static let foo: Int", + Config{ + StaticModifierEnabled: true, + NativeModifierEnabled: true, + }, + ) + + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "invalid static modifier after native modifier", + Pos: ast.Position{Offset: 7, Line: 1, Column: 7}, + }, + }, + errs, + ) + }) + + t.Run("pub static native, enabled", func(t *testing.T) { + + t.Parallel() + + result, errs := parse( + "pub static native let foo: Int", + Config{ + StaticModifierEnabled: true, + NativeModifierEnabled: true, + }, + ) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.FieldDeclaration{ + Access: ast.AccessAll, + Flags: ast.FieldDeclarationFlagsIsStatic | ast.FieldDeclarationFlagsIsNative, + VariableKind: ast.VariableKindConstant, + Identifier: ast.Identifier{ + Identifier: "foo", + Pos: ast.Position{Line: 1, Column: 22, Offset: 22}, + }, + TypeAnnotation: &ast.TypeAnnotation{ + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Int", + Pos: ast.Position{Line: 1, Column: 27, Offset: 27}, + }, + }, + StartPos: ast.Position{Line: 1, Column: 27, Offset: 27}, + }, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 29, Offset: 29}, + }, + }, + result, + ) + }) + + t.Run("pub static native, disabled", func(t *testing.T) { + + t.Parallel() + + _, errs := parse("pub static native let foo: Int", Config{}) + + // For now, leading unknown identifiers are valid. + // This will be rejected in Stable Cadence. + + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "unexpected identifier", + Pos: ast.Position{Offset: 11, Line: 1, Column: 11}, + }, + }, + errs, + ) + }) + +} + +func TestParseCompositeDeclaration(t *testing.T) { + + t.Parallel() + + t.Run("struct, no conformances", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseDeclarations(" pub struct S { }") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.CompositeDeclaration{ + Access: ast.AccessAll, + CompositeKind: common.CompositeKindStructure, + Identifier: ast.Identifier{ + Identifier: "S", + Pos: ast.Position{Line: 1, Column: 12, Offset: 12}, + }, + Members: &ast.Members{}, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + EndPos: ast.Position{Line: 1, Column: 16, Offset: 16}, + }, + }, + }, + result, + ) + }) + + t.Run("resource, one conformance", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseDeclarations(" pub resource R : RI { }") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.CompositeDeclaration{ + Access: ast.AccessAll, + CompositeKind: common.CompositeKindResource, + Identifier: ast.Identifier{ + Identifier: "R", + Pos: ast.Position{Line: 1, Column: 14, Offset: 14}, + }, + Conformances: []*ast.NominalType{ + { + Identifier: ast.Identifier{ + Identifier: "RI", + Pos: ast.Position{Line: 1, Column: 18, Offset: 18}, + }, + }, + }, + Members: &ast.Members{}, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + EndPos: ast.Position{Line: 1, Column: 23, Offset: 23}, + }, + }, + }, + result, + ) + }) + + t.Run("struct, with fields, functions, and special functions", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseDeclarations(` + struct Test { + pub(set) var foo: Int + + init(foo: Int) { + self.foo = foo + } + + pub fun getFoo(): Int { + return self.foo + } + } + `) + + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.CompositeDeclaration{ + Access: ast.AccessNotSpecified, + CompositeKind: common.CompositeKindStructure, + Identifier: ast.Identifier{ + Identifier: "Test", + Pos: ast.Position{Offset: 18, Line: 2, Column: 17}, + }, + Members: ast.NewUnmeteredMembers( + []ast.Declaration{ + &ast.FieldDeclaration{ + Access: ast.PubSettableLegacy, + VariableKind: ast.VariableKindVariable, + Identifier: ast.Identifier{ + Identifier: "foo", + Pos: ast.Position{Offset: 52, Line: 3, Column: 27}, + }, + TypeAnnotation: &ast.TypeAnnotation{ + IsResource: false, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Int", + Pos: ast.Position{Offset: 57, Line: 3, Column: 32}, + }, + }, + StartPos: ast.Position{Offset: 57, Line: 3, Column: 32}, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 39, Line: 3, Column: 14}, + EndPos: ast.Position{Offset: 59, Line: 3, Column: 34}, + }, + }, + &ast.SpecialFunctionDeclaration{ + Kind: common.DeclarationKindInitializer, + FunctionDeclaration: &ast.FunctionDeclaration{ + Access: ast.AccessNotSpecified, + Identifier: ast.Identifier{ + Identifier: "init", + Pos: ast.Position{Offset: 76, Line: 5, Column: 14}, + }, + ParameterList: &ast.ParameterList{ + Parameters: []*ast.Parameter{ + { + Label: "", + Identifier: ast.Identifier{ + Identifier: "foo", + Pos: ast.Position{Offset: 81, Line: 5, Column: 19}, + }, + TypeAnnotation: &ast.TypeAnnotation{ + IsResource: false, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Int", + Pos: ast.Position{Offset: 86, Line: 5, Column: 24}, + }, + }, + StartPos: ast.Position{Offset: 86, Line: 5, Column: 24}, + }, + StartPos: ast.Position{Offset: 81, Line: 5, Column: 19}, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 80, Line: 5, Column: 18}, + EndPos: ast.Position{Offset: 89, Line: 5, Column: 27}, + }, + }, + FunctionBlock: &ast.FunctionBlock{ + Block: &ast.Block{ + Statements: []ast.Statement{ + &ast.AssignmentStatement{ + Target: &ast.MemberExpression{ + Expression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "self", + Pos: ast.Position{Offset: 111, Line: 6, Column: 18}, + }, + }, + AccessPos: ast.Position{Offset: 115, Line: 6, Column: 22}, + Identifier: ast.Identifier{ + Identifier: "foo", + Pos: ast.Position{Offset: 116, Line: 6, Column: 23}, + }, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationCopy, + Pos: ast.Position{Offset: 120, Line: 6, Column: 27}, + }, + Value: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "foo", + Pos: ast.Position{Offset: 122, Line: 6, Column: 29}, + }, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 91, Line: 5, Column: 29}, + EndPos: ast.Position{Offset: 140, Line: 7, Column: 14}, + }, + }, + }, + StartPos: ast.Position{Offset: 76, Line: 5, Column: 14}, + }, + }, + &ast.FunctionDeclaration{ + Access: ast.AccessAll, + Identifier: ast.Identifier{ + Identifier: "getFoo", + Pos: ast.Position{Offset: 165, Line: 9, Column: 22}, + }, + ParameterList: &ast.ParameterList{ + Range: ast.Range{ + StartPos: ast.Position{Offset: 171, Line: 9, Column: 28}, + EndPos: ast.Position{Offset: 172, Line: 9, Column: 29}, + }, + }, + ReturnTypeAnnotation: &ast.TypeAnnotation{ + IsResource: false, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Int", + Pos: ast.Position{Offset: 175, Line: 9, Column: 32}, + }, + }, + StartPos: ast.Position{Offset: 175, Line: 9, Column: 32}, + }, + FunctionBlock: &ast.FunctionBlock{ + Block: &ast.Block{ + Statements: []ast.Statement{ + &ast.ReturnStatement{ + Expression: &ast.MemberExpression{ + Expression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "self", + Pos: ast.Position{Offset: 206, Line: 10, Column: 25}, + }, + }, + AccessPos: ast.Position{Offset: 210, Line: 10, Column: 29}, + Identifier: ast.Identifier{ + Identifier: "foo", + Pos: ast.Position{Offset: 211, Line: 10, Column: 30}, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 199, Line: 10, Column: 18}, + EndPos: ast.Position{Offset: 213, Line: 10, Column: 32}, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 179, Line: 9, Column: 36}, + EndPos: ast.Position{Offset: 229, Line: 11, Column: 14}, + }, + }, + }, + StartPos: ast.Position{Offset: 157, Line: 9, Column: 14}, + }, + }, + ), + Range: ast.Range{ + StartPos: ast.Position{Offset: 11, Line: 2, Column: 10}, + EndPos: ast.Position{Offset: 241, Line: 12, Column: 10}, + }, + }, + }, + result, + ) + }) +} + +func TestParseAttachmentDeclaration(t *testing.T) { + + t.Parallel() + + t.Run("no conformances", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseDeclarations("pub attachment E for S {} ") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.AttachmentDeclaration{ + Access: ast.AccessAll, + Identifier: ast.Identifier{ + Identifier: "E", + Pos: ast.Position{Line: 1, Column: 15, Offset: 15}, + }, + BaseType: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "S", + Pos: ast.Position{Line: 1, Column: 21, Offset: 21}, + }, + }, + Members: &ast.Members{}, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 24, Offset: 24}, + }, + }, + }, + result, + ) + }) + + t.Run("nested in contract", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseDeclarations(` + contract Test { + pub attachment E for S {} + }`) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.CompositeDeclaration{ + Access: ast.AccessNotSpecified, + CompositeKind: common.CompositeKindContract, + Identifier: ast.Identifier{ + Identifier: "Test", + Pos: ast.Position{Line: 2, Column: 11, Offset: 12}, + }, + Range: ast.Range{ + StartPos: ast.Position{Line: 2, Column: 2, Offset: 3}, + EndPos: ast.Position{Line: 4, Column: 2, Offset: 50}, + }, + Members: ast.NewUnmeteredMembers( + []ast.Declaration{ + &ast.AttachmentDeclaration{ + Access: ast.AccessAll, + Identifier: ast.Identifier{ + Identifier: "E", + Pos: ast.Position{Line: 3, Column: 18, Offset: 37}, + }, + BaseType: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "S", + Pos: ast.Position{Line: 3, Column: 24, Offset: 43}, + }, + }, + Members: &ast.Members{}, + Range: ast.Range{ + StartPos: ast.Position{Line: 3, Column: 3, Offset: 22}, + EndPos: ast.Position{Line: 3, Column: 27, Offset: 46}, + }, + }, + }, + ), + }, + }, + result, + ) + }) + + t.Run("missing base type", func(t *testing.T) { + + t.Parallel() + + _, errs := testParseDeclarations("attachment E {} ") + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "expected 'for', got '{'", + Pos: ast.Position{Offset: 13, Line: 1, Column: 13}, + }, + }, + errs, + ) + }) + + t.Run("one conformances", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseDeclarations("pub attachment E for S: I {} ") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.AttachmentDeclaration{ + Access: ast.AccessAll, + Identifier: ast.Identifier{ + Identifier: "E", + Pos: ast.Position{Line: 1, Column: 15, Offset: 15}, + }, + BaseType: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "S", + Pos: ast.Position{Line: 1, Column: 21, Offset: 21}, + }, + }, + Members: &ast.Members{}, + Conformances: []*ast.NominalType{ + ast.NewNominalType( + nil, + ast.Identifier{ + Identifier: "I", + Pos: ast.Position{Line: 1, Column: 24, Offset: 24}, + }, + nil, + ), + }, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 27, Offset: 27}, + }, + }, + }, + result, + ) + }) + + t.Run("two conformances", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseDeclarations("pub attachment E for S: I1, I2 {} ") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.AttachmentDeclaration{ + Access: ast.AccessAll, + Identifier: ast.Identifier{ + Identifier: "E", + Pos: ast.Position{Line: 1, Column: 15, Offset: 15}, + }, + BaseType: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "S", + Pos: ast.Position{Line: 1, Column: 21, Offset: 21}, + }, + }, + Members: &ast.Members{}, + Conformances: []*ast.NominalType{ + ast.NewNominalType( + nil, + ast.Identifier{ + Identifier: "I1", + Pos: ast.Position{Line: 1, Column: 24, Offset: 24}, + }, + nil, + ), + ast.NewNominalType( + nil, + ast.Identifier{ + Identifier: "I2", + Pos: ast.Position{Line: 1, Column: 28, Offset: 28}, + }, + nil, + ), + }, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 32, Offset: 32}, + }, + }, + }, + result, + ) + }) + + t.Run("fields, functions and special functions", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseDeclarations(`pub attachment E for S { + pub(set) var foo: Int + init() {} + destroy() {} + pub fun getFoo(): Int {} + }`) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.AttachmentDeclaration{ + Access: ast.AccessAll, + Identifier: ast.Identifier{ + Identifier: "E", + Pos: ast.Position{Line: 1, Column: 15, Offset: 15}, + }, + BaseType: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "S", + Pos: ast.Position{Line: 1, Column: 21, Offset: 21}, + }, + }, + Members: ast.NewUnmeteredMembers( + []ast.Declaration{ + &ast.FieldDeclaration{ + Access: ast.PubSettableLegacy, + VariableKind: ast.VariableKindVariable, + Identifier: ast.Identifier{ + Identifier: "foo", + Pos: ast.Position{Offset: 41, Line: 2, Column: 16}, + }, + TypeAnnotation: &ast.TypeAnnotation{ + IsResource: false, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Int", + Pos: ast.Position{Offset: 46, Line: 2, Column: 21}, + }, + }, + StartPos: ast.Position{Offset: 46, Line: 2, Column: 21}, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 28, Line: 2, Column: 3}, + EndPos: ast.Position{Offset: 48, Line: 2, Column: 23}, + }, + }, + &ast.SpecialFunctionDeclaration{ + Kind: common.DeclarationKindInitializer, + FunctionDeclaration: &ast.FunctionDeclaration{ + Access: ast.AccessNotSpecified, + Identifier: ast.Identifier{ + Identifier: "init", + Pos: ast.Position{Offset: 53, Line: 3, Column: 3}, + }, + ParameterList: &ast.ParameterList{ + Range: ast.Range{ + StartPos: ast.Position{Offset: 57, Line: 3, Column: 7}, + EndPos: ast.Position{Offset: 58, Line: 3, Column: 8}, + }, + }, + FunctionBlock: &ast.FunctionBlock{ + Block: &ast.Block{ + Range: ast.Range{ + StartPos: ast.Position{Offset: 60, Line: 3, Column: 10}, + EndPos: ast.Position{Offset: 61, Line: 3, Column: 11}, + }, + }, + }, + StartPos: ast.Position{Offset: 53, Line: 3, Column: 3}, + }, + }, + &ast.SpecialFunctionDeclaration{ + Kind: common.DeclarationKindDestructorLegacy, + FunctionDeclaration: &ast.FunctionDeclaration{ + Access: ast.AccessNotSpecified, + Identifier: ast.Identifier{ + Identifier: "destroy", + Pos: ast.Position{Offset: 66, Line: 4, Column: 3}, + }, + ParameterList: &ast.ParameterList{ + Range: ast.Range{ + StartPos: ast.Position{Offset: 73, Line: 4, Column: 10}, + EndPos: ast.Position{Offset: 74, Line: 4, Column: 11}, + }, + }, + FunctionBlock: &ast.FunctionBlock{ + Block: &ast.Block{ + Range: ast.Range{ + StartPos: ast.Position{Offset: 76, Line: 4, Column: 13}, + EndPos: ast.Position{Offset: 77, Line: 4, Column: 14}, + }, + }, + }, + StartPos: ast.Position{Offset: 66, Line: 4, Column: 3}, + }, + }, + &ast.FunctionDeclaration{ + Access: ast.AccessAll, + Identifier: ast.Identifier{ + Identifier: "getFoo", + Pos: ast.Position{Offset: 90, Line: 5, Column: 11}, + }, + ParameterList: &ast.ParameterList{ + Range: ast.Range{ + StartPos: ast.Position{Offset: 96, Line: 5, Column: 17}, + EndPos: ast.Position{Offset: 97, Line: 5, Column: 18}, + }, + }, + ReturnTypeAnnotation: &ast.TypeAnnotation{ + IsResource: false, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Int", + Pos: ast.Position{Offset: 100, Line: 5, Column: 21}, + }, + }, + StartPos: ast.Position{Offset: 100, Line: 5, Column: 21}, + }, + FunctionBlock: &ast.FunctionBlock{ + Block: &ast.Block{ + Range: ast.Range{ + StartPos: ast.Position{Offset: 104, Line: 5, Column: 25}, + EndPos: ast.Position{Offset: 105, Line: 5, Column: 26}, + }, + }, + }, + StartPos: ast.Position{Offset: 82, Line: 5, Column: 3}, + }, + }, + ), + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 6, Column: 2, Offset: 109}, + }, + }, + }, + result, + ) + }) +} + +func TestParseInterfaceDeclaration(t *testing.T) { + + t.Parallel() + + t.Run("struct, no conformances", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseDeclarations(" pub struct interface S { }") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.InterfaceDeclaration{ + Access: ast.AccessAll, + CompositeKind: common.CompositeKindStructure, + Identifier: ast.Identifier{ + Identifier: "S", + Pos: ast.Position{Line: 1, Column: 22, Offset: 22}, + }, + Members: &ast.Members{}, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + EndPos: ast.Position{Line: 1, Column: 26, Offset: 26}, + }, + }, + }, + result, + ) + }) + + t.Run("struct, interface keyword as name", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseDeclarations(" pub struct interface interface { }") + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "expected interface name, got keyword \"interface\"", + Pos: ast.Position{Offset: 22, Line: 1, Column: 22}, + }, + }, + errs, + ) + + var expected []ast.Declaration + + utils.AssertEqualWithDiff(t, + expected, + result, + ) + }) + + t.Run("struct, with fields, functions, and special functions; with and without blocks", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseDeclarations(` + struct interface Test { + pub(set) var foo: Int + + init(foo: Int) + + pub fun getFoo(): Int + + pub fun getBar(): Int {} + + destroy() {} + } + `) + + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.InterfaceDeclaration{ + Access: ast.AccessNotSpecified, + CompositeKind: common.CompositeKindStructure, + Identifier: ast.Identifier{ + Identifier: "Test", + Pos: ast.Position{Offset: 28, Line: 2, Column: 27}, + }, + Members: ast.NewUnmeteredMembers( + []ast.Declaration{ + &ast.FieldDeclaration{ + Access: ast.PubSettableLegacy, + VariableKind: ast.VariableKindVariable, + Identifier: ast.Identifier{ + Identifier: "foo", + Pos: ast.Position{Offset: 62, Line: 3, Column: 27}, + }, + TypeAnnotation: &ast.TypeAnnotation{ + IsResource: false, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Int", + Pos: ast.Position{Offset: 67, Line: 3, Column: 32}, + }, + }, + StartPos: ast.Position{Offset: 67, Line: 3, Column: 32}, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 49, Line: 3, Column: 14}, + EndPos: ast.Position{Offset: 69, Line: 3, Column: 34}, + }, + }, + &ast.SpecialFunctionDeclaration{ + Kind: common.DeclarationKindInitializer, + FunctionDeclaration: &ast.FunctionDeclaration{ + Access: ast.AccessNotSpecified, + Identifier: ast.Identifier{ + Identifier: "init", + Pos: ast.Position{Offset: 86, Line: 5, Column: 14}, + }, + ParameterList: &ast.ParameterList{ + Parameters: []*ast.Parameter{ + { + Label: "", + Identifier: ast.Identifier{ + Identifier: "foo", + Pos: ast.Position{Offset: 91, Line: 5, Column: 19}, + }, + TypeAnnotation: &ast.TypeAnnotation{ + IsResource: false, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Int", + Pos: ast.Position{Offset: 96, Line: 5, Column: 24}, + }, + }, + StartPos: ast.Position{Offset: 96, Line: 5, Column: 24}, + }, + StartPos: ast.Position{Offset: 91, Line: 5, Column: 19}, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 90, Line: 5, Column: 18}, + EndPos: ast.Position{Offset: 99, Line: 5, Column: 27}, + }, + }, + StartPos: ast.Position{Offset: 86, Line: 5, Column: 14}, + }, + }, + &ast.FunctionDeclaration{ + Access: ast.AccessAll, + Identifier: ast.Identifier{ + Identifier: "getFoo", + Pos: ast.Position{Offset: 124, Line: 7, Column: 22}, + }, + ParameterList: &ast.ParameterList{ + Range: ast.Range{ + StartPos: ast.Position{Offset: 130, Line: 7, Column: 28}, + EndPos: ast.Position{Offset: 131, Line: 7, Column: 29}, + }, + }, + ReturnTypeAnnotation: &ast.TypeAnnotation{ + IsResource: false, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Int", + Pos: ast.Position{Offset: 134, Line: 7, Column: 32}, + }, + }, + StartPos: ast.Position{Offset: 134, Line: 7, Column: 32}, + }, + StartPos: ast.Position{Offset: 116, Line: 7, Column: 14}, + }, + &ast.FunctionDeclaration{ + Access: ast.AccessAll, + Identifier: ast.Identifier{ + Identifier: "getBar", + Pos: ast.Position{Offset: 161, Line: 9, Column: 22}, + }, + ParameterList: &ast.ParameterList{ + Range: ast.Range{ + StartPos: ast.Position{Offset: 167, Line: 9, Column: 28}, + EndPos: ast.Position{Offset: 168, Line: 9, Column: 29}, + }, + }, + ReturnTypeAnnotation: &ast.TypeAnnotation{ + IsResource: false, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Int", + Pos: ast.Position{Offset: 171, Line: 9, Column: 32}, + }, + }, + StartPos: ast.Position{Offset: 171, Line: 9, Column: 32}, + }, + FunctionBlock: &ast.FunctionBlock{ + Block: &ast.Block{ + Range: ast.Range{ + StartPos: ast.Position{Offset: 175, Line: 9, Column: 36}, + EndPos: ast.Position{Offset: 176, Line: 9, Column: 37}, + }, + }, + }, + StartPos: ast.Position{Offset: 153, Line: 9, Column: 14}, + }, + &ast.SpecialFunctionDeclaration{ + Kind: common.DeclarationKindDestructorLegacy, + FunctionDeclaration: &ast.FunctionDeclaration{ + Access: ast.AccessNotSpecified, + Identifier: ast.Identifier{ + Identifier: "destroy", + Pos: ast.Position{Offset: 193, Line: 11, Column: 14}, + }, + ParameterList: &ast.ParameterList{ + Range: ast.Range{ + StartPos: ast.Position{Offset: 200, Line: 11, Column: 21}, + EndPos: ast.Position{Offset: 201, Line: 11, Column: 22}, + }, + }, + FunctionBlock: &ast.FunctionBlock{ + Block: &ast.Block{ + Range: ast.Range{ + StartPos: ast.Position{Offset: 203, Line: 11, Column: 24}, + EndPos: ast.Position{Offset: 204, Line: 11, Column: 25}, + }, + }, + }, + StartPos: ast.Position{Offset: 193, Line: 11, Column: 14}, + }, + }, + }, + ), + Range: ast.Range{ + StartPos: ast.Position{Offset: 11, Line: 2, Column: 10}, + EndPos: ast.Position{Offset: 216, Line: 12, Column: 10}, + }, + }, + }, + result, + ) + }) +} + +func TestParseEnumDeclaration(t *testing.T) { + + t.Parallel() + + t.Run("enum, two cases one one line", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseDeclarations(" pub enum E { case c ; pub case d }") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.CompositeDeclaration{ + Access: ast.AccessAll, + CompositeKind: common.CompositeKindEnum, + Identifier: ast.Identifier{ + Identifier: "E", + Pos: ast.Position{Line: 1, Column: 10, Offset: 10}, + }, + Members: ast.NewUnmeteredMembers( + []ast.Declaration{ + &ast.EnumCaseDeclaration{ + Access: ast.AccessNotSpecified, + Identifier: ast.Identifier{ + Identifier: "c", + Pos: ast.Position{Line: 1, Column: 19, Offset: 19}, + }, + StartPos: ast.Position{Line: 1, Column: 14, Offset: 14}, + }, + &ast.EnumCaseDeclaration{ + Access: ast.AccessAll, + Identifier: ast.Identifier{ + Identifier: "d", + Pos: ast.Position{Line: 1, Column: 32, Offset: 32}, + }, + StartPos: ast.Position{Line: 1, Column: 23, Offset: 23}, + }, + }, + ), + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + EndPos: ast.Position{Line: 1, Column: 34, Offset: 34}, + }, + }, + }, + result, + ) + }) + + t.Run("enum case with static modifier, enabled", func(t *testing.T) { + + t.Parallel() + + _, errs := ParseDeclarations( + nil, + []byte(" enum E { static case e }"), + Config{ + StaticModifierEnabled: true, + }, + ) + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "invalid static modifier for enum case", + Pos: ast.Position{Offset: 10, Line: 1, Column: 10}, + }, + }, + errs, + ) + }) + + t.Run("enum case with static modifier, disabled", func(t *testing.T) { + + t.Parallel() + + _, errs := testParseDeclarations(" enum E { static case e }") + + // For now, leading unknown identifiers are valid. + // This will be rejected in Stable Cadence. + + require.Empty(t, errs) + }) + + t.Run("enum case with native modifier, enabled", func(t *testing.T) { + + t.Parallel() + + _, errs := ParseDeclarations( + nil, + []byte(" enum E { native case e }"), + Config{ + NativeModifierEnabled: true, + }, + ) + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "invalid native modifier for enum case", + Pos: ast.Position{Offset: 10, Line: 1, Column: 10}, + }, + }, + errs, + ) + }) + + t.Run("enum case with native modifier, disabled", func(t *testing.T) { + + t.Parallel() + + _, errs := testParseDeclarations(" enum E { native case e }") + + // For now, leading unknown identifiers are valid. + // This will be rejected in Stable Cadence. + + require.Empty(t, errs) + }) +} + +func TestParseTransactionDeclaration(t *testing.T) { + + t.Parallel() + + t.Run("no prepare, execute", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseDeclarations("transaction { execute {} }") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.TransactionDeclaration{ + Execute: &ast.SpecialFunctionDeclaration{ + Kind: common.DeclarationKindExecute, + FunctionDeclaration: &ast.FunctionDeclaration{ + Access: ast.AccessNotSpecified, + Identifier: ast.Identifier{ + Identifier: "execute", + Pos: ast.Position{Offset: 14, Line: 1, Column: 14}, + }, + FunctionBlock: &ast.FunctionBlock{ + Block: &ast.Block{ + Range: ast.Range{ + StartPos: ast.Position{Offset: 22, Line: 1, Column: 22}, + EndPos: ast.Position{Offset: 23, Line: 1, Column: 23}, + }, + }, + }, + StartPos: ast.Position{Offset: 14, Line: 1, Column: 14}, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 25, Offset: 25}, + }, + }, + }, + result, + ) + }) + + t.Run("EmptyTransaction", func(t *testing.T) { + + const code = ` + transaction {} + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.TransactionDeclaration{ + Fields: nil, + Prepare: nil, + PreConditions: nil, + PostConditions: nil, + Execute: nil, + Range: ast.Range{ + StartPos: ast.Position{Offset: 5, Line: 2, Column: 4}, + EndPos: ast.Position{Offset: 18, Line: 2, Column: 17}, + }, + }, + }, + result.Declarations(), + ) + }) + + t.Run("SimpleTransaction", func(t *testing.T) { + const code = ` + transaction { + + var x: Int + + prepare(signer: AuthAccount) { + x = 0 + } + + execute { + x = 1 + 1 + } + } + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.TransactionDeclaration{ + Fields: []*ast.FieldDeclaration{ + { + Access: ast.AccessNotSpecified, + VariableKind: ast.VariableKindVariable, + Identifier: ast.Identifier{ + Identifier: "x", + Pos: ast.Position{Offset: 30, Line: 4, Column: 10}, + }, + TypeAnnotation: &ast.TypeAnnotation{ + IsResource: false, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Int", + Pos: ast.Position{Offset: 33, Line: 4, Column: 13}, + }, + }, + StartPos: ast.Position{Offset: 33, Line: 4, Column: 13}, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 26, Line: 4, Column: 6}, + EndPos: ast.Position{Offset: 35, Line: 4, Column: 15}, + }, + }, + }, + Prepare: &ast.SpecialFunctionDeclaration{ + Kind: common.DeclarationKindPrepare, + FunctionDeclaration: &ast.FunctionDeclaration{ + Access: ast.AccessNotSpecified, + Identifier: ast.Identifier{ + Identifier: "prepare", + Pos: ast.Position{Offset: 44, Line: 6, Column: 6}, + }, + ParameterList: &ast.ParameterList{ + Parameters: []*ast.Parameter{ + { + Label: "", + Identifier: ast.Identifier{ + Identifier: "signer", + Pos: ast.Position{Offset: 52, Line: 6, Column: 14}, + }, + TypeAnnotation: &ast.TypeAnnotation{ + IsResource: false, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "AuthAccount", + Pos: ast.Position{Offset: 60, Line: 6, Column: 22}, + }, + }, + StartPos: ast.Position{Offset: 60, Line: 6, Column: 22}, + }, + StartPos: ast.Position{Offset: 52, Line: 6, Column: 14}, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 51, Line: 6, Column: 13}, + EndPos: ast.Position{Offset: 71, Line: 6, Column: 33}, + }, + }, + ReturnTypeAnnotation: nil, + FunctionBlock: &ast.FunctionBlock{ + Block: &ast.Block{ + Statements: []ast.Statement{ + &ast.AssignmentStatement{ + Target: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "x", + Pos: ast.Position{Offset: 86, Line: 7, Column: 11}, + }, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationCopy, + Pos: ast.Position{Offset: 88, Line: 7, Column: 13}, + }, + Value: &ast.IntegerExpression{ + PositiveLiteral: []byte("0"), + Value: new(big.Int), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 90, Line: 7, Column: 15}, + EndPos: ast.Position{Offset: 90, Line: 7, Column: 15}, + }, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 73, Line: 6, Column: 35}, + EndPos: ast.Position{Offset: 95, Line: 8, Column: 3}, + }, + }, + PreConditions: nil, + PostConditions: nil, + }, + StartPos: ast.Position{Offset: 44, Line: 6, Column: 6}, + }, + }, + PreConditions: nil, + PostConditions: nil, + Execute: &ast.SpecialFunctionDeclaration{ + Kind: common.DeclarationKindExecute, + FunctionDeclaration: &ast.FunctionDeclaration{ + Access: ast.AccessNotSpecified, + Identifier: ast.Identifier{ + Identifier: "execute", + Pos: ast.Position{Offset: 104, Line: 10, Column: 6}, + }, + ReturnTypeAnnotation: nil, + FunctionBlock: &ast.FunctionBlock{ + Block: &ast.Block{ + Statements: []ast.Statement{ + &ast.AssignmentStatement{ + Target: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "x", + Pos: ast.Position{Offset: 125, Line: 11, Column: 11}, + }, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationCopy, + Pos: ast.Position{Offset: 127, Line: 11, Column: 13}, + }, + Value: &ast.BinaryExpression{ + Operation: ast.OperationPlus, + Left: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 129, Line: 11, Column: 15}, + EndPos: ast.Position{Offset: 129, Line: 11, Column: 15}, + }, + }, + Right: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 133, Line: 11, Column: 19}, + EndPos: ast.Position{Offset: 133, Line: 11, Column: 19}, + }, + }, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 112, Line: 10, Column: 14}, + EndPos: ast.Position{Offset: 138, Line: 12, Column: 3}, + }, + }, + PreConditions: nil, + PostConditions: nil, + }, + StartPos: ast.Position{Offset: 104, Line: 10, Column: 6}, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 5, Line: 2, Column: 4}, + EndPos: ast.Position{Offset: 144, Line: 13, Column: 4}, + }, + }, + }, + result.Declarations(), + ) + }) + + t.Run("PreExecutePost", func(t *testing.T) { + const code = ` + transaction { + + var x: Int + + prepare(signer: AuthAccount) { + x = 0 + } + + pre { + x == 0 + } + + execute { + x = 1 + 1 + } + + post { + x == 2 + } + } + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.TransactionDeclaration{ + Fields: []*ast.FieldDeclaration{ + { + Access: ast.AccessNotSpecified, + VariableKind: ast.VariableKindVariable, + Identifier: ast.Identifier{ + Identifier: "x", + Pos: ast.Position{Offset: 30, Line: 4, Column: 10}, + }, + TypeAnnotation: &ast.TypeAnnotation{ + IsResource: false, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Int", + Pos: ast.Position{Offset: 33, Line: 4, Column: 13}, + }, + }, + StartPos: ast.Position{Offset: 33, Line: 4, Column: 13}, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 26, Line: 4, Column: 6}, + EndPos: ast.Position{Offset: 35, Line: 4, Column: 15}, + }, + }, + }, + Prepare: &ast.SpecialFunctionDeclaration{ + Kind: common.DeclarationKindPrepare, + FunctionDeclaration: &ast.FunctionDeclaration{ + Access: ast.AccessNotSpecified, + Identifier: ast.Identifier{ + Identifier: "prepare", + Pos: ast.Position{Offset: 44, Line: 6, Column: 6}, + }, + ParameterList: &ast.ParameterList{ + Parameters: []*ast.Parameter{ + { + Label: "", + Identifier: ast.Identifier{ + Identifier: "signer", + Pos: ast.Position{Offset: 52, Line: 6, Column: 14}, + }, + TypeAnnotation: &ast.TypeAnnotation{ + IsResource: false, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "AuthAccount", + Pos: ast.Position{Offset: 60, Line: 6, Column: 22}, + }, + }, + StartPos: ast.Position{Offset: 60, Line: 6, Column: 22}, + }, + StartPos: ast.Position{Offset: 52, Line: 6, Column: 14}, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 51, Line: 6, Column: 13}, + EndPos: ast.Position{Offset: 71, Line: 6, Column: 33}, + }, + }, + ReturnTypeAnnotation: nil, + FunctionBlock: &ast.FunctionBlock{ + Block: &ast.Block{ + Statements: []ast.Statement{ + &ast.AssignmentStatement{ + Target: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "x", + Pos: ast.Position{Offset: 86, Line: 7, Column: 11}, + }, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationCopy, + Pos: ast.Position{Offset: 88, Line: 7, Column: 13}, + }, + Value: &ast.IntegerExpression{ + PositiveLiteral: []byte("0"), + Value: new(big.Int), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 90, Line: 7, Column: 15}, + EndPos: ast.Position{Offset: 90, Line: 7, Column: 15}, + }, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 73, Line: 6, Column: 35}, + EndPos: ast.Position{Offset: 95, Line: 8, Column: 3}, + }, + }, + PreConditions: nil, + PostConditions: nil, + }, + StartPos: ast.Position{Offset: 44, Line: 6, Column: 6}, + }, + }, + PreConditions: &ast.Conditions{ + &ast.TestCondition{ + Test: &ast.BinaryExpression{ + Operation: ast.OperationEqual, + Left: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "x", + Pos: ast.Position{Offset: 117, Line: 11, Column: 10}, + }, + }, + Right: &ast.IntegerExpression{ + PositiveLiteral: []byte("0"), + Value: new(big.Int), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 122, Line: 11, Column: 15}, + EndPos: ast.Position{Offset: 122, Line: 11, Column: 15}, + }, + }, + }, + }, + }, + PostConditions: &ast.Conditions{ + &ast.TestCondition{ + Test: &ast.BinaryExpression{ + Operation: ast.OperationEqual, + Left: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "x", + Pos: ast.Position{Offset: 197, Line: 19, Column: 11}, + }, + }, + Right: &ast.IntegerExpression{ + PositiveLiteral: []byte("2"), + Value: big.NewInt(2), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 202, Line: 19, Column: 16}, + EndPos: ast.Position{Offset: 202, Line: 19, Column: 16}, + }, + }, + }, + }, + }, + Execute: &ast.SpecialFunctionDeclaration{ + Kind: common.DeclarationKindExecute, + FunctionDeclaration: &ast.FunctionDeclaration{ + Access: ast.AccessNotSpecified, + Identifier: ast.Identifier{ + Identifier: "execute", + Pos: ast.Position{Offset: 136, Line: 14, Column: 6}, + }, + ReturnTypeAnnotation: nil, + FunctionBlock: &ast.FunctionBlock{ + Block: &ast.Block{ + Statements: []ast.Statement{ + &ast.AssignmentStatement{ + Target: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "x", + Pos: ast.Position{Offset: 157, Line: 15, Column: 11}, + }, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationCopy, + Pos: ast.Position{Offset: 159, Line: 15, Column: 13}, + }, + Value: &ast.BinaryExpression{ + Operation: ast.OperationPlus, + Left: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 161, Line: 15, Column: 15}, + EndPos: ast.Position{Offset: 161, Line: 15, Column: 15}, + }, + }, + Right: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 165, Line: 15, Column: 19}, + EndPos: ast.Position{Offset: 165, Line: 15, Column: 19}, + }, + }, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 144, Line: 14, Column: 14}, + EndPos: ast.Position{Offset: 170, Line: 16, Column: 3}, + }, + }, + PreConditions: nil, + PostConditions: nil, + }, + StartPos: ast.Position{Offset: 136, Line: 14, Column: 6}, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 5, Line: 2, Column: 4}, + EndPos: ast.Position{Offset: 219, Line: 21, Column: 4}, + }, + }, + }, + result.Declarations(), + ) + }) + + t.Run("PrePostExecute", func(t *testing.T) { + const code = ` + transaction { + + var x: Int + + prepare(signer: AuthAccount) { + x = 0 + } + + pre { + x == 0 + } + + post { + x == 2 + } + + execute { + x = 1 + 1 + } + } + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.TransactionDeclaration{ + Fields: []*ast.FieldDeclaration{ + { + Access: ast.AccessNotSpecified, + VariableKind: ast.VariableKindVariable, + Identifier: ast.Identifier{ + Identifier: "x", + Pos: ast.Position{Offset: 30, Line: 4, Column: 10}, + }, + TypeAnnotation: &ast.TypeAnnotation{ + IsResource: false, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Int", + Pos: ast.Position{Offset: 33, Line: 4, Column: 13}, + }, + }, + StartPos: ast.Position{Offset: 33, Line: 4, Column: 13}, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 26, Line: 4, Column: 6}, + EndPos: ast.Position{Offset: 35, Line: 4, Column: 15}, + }, + }, + }, + Prepare: &ast.SpecialFunctionDeclaration{ + Kind: common.DeclarationKindPrepare, + FunctionDeclaration: &ast.FunctionDeclaration{ + Access: ast.AccessNotSpecified, + Identifier: ast.Identifier{ + Identifier: "prepare", + Pos: ast.Position{Offset: 44, Line: 6, Column: 6}, + }, + ParameterList: &ast.ParameterList{ + Parameters: []*ast.Parameter{ + { + Label: "", + Identifier: ast.Identifier{ + Identifier: "signer", + Pos: ast.Position{Offset: 52, Line: 6, Column: 14}, + }, + TypeAnnotation: &ast.TypeAnnotation{ + IsResource: false, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "AuthAccount", + Pos: ast.Position{Offset: 60, Line: 6, Column: 22}, + }, + }, + StartPos: ast.Position{Offset: 60, Line: 6, Column: 22}, + }, + StartPos: ast.Position{Offset: 52, Line: 6, Column: 14}, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 51, Line: 6, Column: 13}, + EndPos: ast.Position{Offset: 71, Line: 6, Column: 33}, + }, + }, + ReturnTypeAnnotation: nil, + FunctionBlock: &ast.FunctionBlock{ + Block: &ast.Block{ + Statements: []ast.Statement{ + &ast.AssignmentStatement{ + Target: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "x", + Pos: ast.Position{Offset: 86, Line: 7, Column: 11}, + }, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationCopy, + Pos: ast.Position{Offset: 88, Line: 7, Column: 13}, + }, + Value: &ast.IntegerExpression{ + PositiveLiteral: []byte("0"), + Value: new(big.Int), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 90, Line: 7, Column: 15}, + EndPos: ast.Position{Offset: 90, Line: 7, Column: 15}, + }, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 73, Line: 6, Column: 35}, + EndPos: ast.Position{Offset: 95, Line: 8, Column: 3}, + }, + }, + PreConditions: nil, + PostConditions: nil, + }, + StartPos: ast.Position{Offset: 44, Line: 6, Column: 6}, + }, + }, + PreConditions: &ast.Conditions{ + &ast.TestCondition{ + Test: &ast.BinaryExpression{ + Operation: ast.OperationEqual, + Left: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "x", + Pos: ast.Position{Offset: 117, Line: 11, Column: 10}, + }, + }, + Right: &ast.IntegerExpression{ + PositiveLiteral: []byte("0"), + Value: new(big.Int), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 122, Line: 11, Column: 15}, + EndPos: ast.Position{Offset: 122, Line: 11, Column: 15}, + }, + }, + }, + }, + }, + PostConditions: &ast.Conditions{ + &ast.TestCondition{ + Test: &ast.BinaryExpression{ + Operation: ast.OperationEqual, + Left: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "x", + Pos: ast.Position{Offset: 154, Line: 15, Column: 11}, + }, + }, + Right: &ast.IntegerExpression{ + PositiveLiteral: []byte("2"), + Value: big.NewInt(2), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 159, Line: 15, Column: 16}, + EndPos: ast.Position{Offset: 159, Line: 15, Column: 16}, + }, + }, + }, + }, + }, + Execute: &ast.SpecialFunctionDeclaration{ + Kind: common.DeclarationKindExecute, + FunctionDeclaration: &ast.FunctionDeclaration{ + Access: ast.AccessNotSpecified, + Identifier: ast.Identifier{ + Identifier: "execute", + Pos: ast.Position{Offset: 179, Line: 18, Column: 6}, + }, + ReturnTypeAnnotation: nil, + FunctionBlock: &ast.FunctionBlock{ + Block: &ast.Block{ + Statements: []ast.Statement{ + &ast.AssignmentStatement{ + Target: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "x", + Pos: ast.Position{Offset: 200, Line: 19, Column: 11}, + }, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationCopy, + Pos: ast.Position{Offset: 202, Line: 19, Column: 13}, + }, + Value: &ast.BinaryExpression{ + Operation: ast.OperationPlus, + Left: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 204, Line: 19, Column: 15}, + EndPos: ast.Position{Offset: 204, Line: 19, Column: 15}, + }, + }, + Right: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 208, Line: 19, Column: 19}, + EndPos: ast.Position{Offset: 208, Line: 19, Column: 19}, + }, + }, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 187, Line: 18, Column: 14}, + EndPos: ast.Position{Offset: 213, Line: 20, Column: 3}, + }, + }, + PreConditions: nil, + PostConditions: nil, + }, + StartPos: ast.Position{Offset: 179, Line: 18, Column: 6}, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 5, Line: 2, Column: 4}, + EndPos: ast.Position{Offset: 219, Line: 21, Column: 4}, + }, + }, + }, + result.Declarations(), + ) + }) +} + +func TestParseFunctionAndBlock(t *testing.T) { + + t.Parallel() + + result, errs := testParseDeclarations(` + fun test() { return } + `) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.FunctionDeclaration{ + Access: ast.AccessNotSpecified, + Identifier: ast.Identifier{ + Identifier: "test", + Pos: ast.Position{Offset: 10, Line: 2, Column: 9}, + }, + ParameterList: &ast.ParameterList{ + Range: ast.Range{ + StartPos: ast.Position{Offset: 14, Line: 2, Column: 13}, + EndPos: ast.Position{Offset: 15, Line: 2, Column: 14}, + }, + }, + FunctionBlock: &ast.FunctionBlock{ + Block: &ast.Block{ + Statements: []ast.Statement{ + &ast.ReturnStatement{ + Range: ast.Range{ + StartPos: ast.Position{Offset: 19, Line: 2, Column: 18}, + EndPos: ast.Position{Offset: 24, Line: 2, Column: 23}, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 17, Line: 2, Column: 16}, + EndPos: ast.Position{Offset: 26, Line: 2, Column: 25}, + }, + }, + }, + StartPos: ast.Position{Offset: 6, Line: 2, Column: 5}, + }, + }, + result, + ) +} + +func TestParseFunctionParameterWithoutLabel(t *testing.T) { + + t.Parallel() + + result, errs := testParseDeclarations(` + fun test(x: Int) { } + `) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.FunctionDeclaration{ + Access: ast.AccessNotSpecified, + Identifier: ast.Identifier{ + Identifier: "test", + Pos: ast.Position{Offset: 10, Line: 2, Column: 9}, + }, + ParameterList: &ast.ParameterList{ + Parameters: []*ast.Parameter{ + { + Label: "", + Identifier: ast.Identifier{ + Identifier: "x", + Pos: ast.Position{Offset: 15, Line: 2, Column: 14}, + }, + TypeAnnotation: &ast.TypeAnnotation{ + IsResource: false, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Int", + Pos: ast.Position{Offset: 18, Line: 2, Column: 17}, + }, + }, + StartPos: ast.Position{Offset: 18, Line: 2, Column: 17}, + }, + StartPos: ast.Position{Offset: 15, Line: 2, Column: 14}, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 14, Line: 2, Column: 13}, + EndPos: ast.Position{Offset: 21, Line: 2, Column: 20}, + }, + }, + FunctionBlock: &ast.FunctionBlock{ + Block: &ast.Block{ + Range: ast.Range{ + StartPos: ast.Position{Offset: 23, Line: 2, Column: 22}, + EndPos: ast.Position{Offset: 25, Line: 2, Column: 24}, + }, + }, + }, + StartPos: ast.Position{Offset: 6, Line: 2, Column: 5}, + }, + }, + result, + ) +} + +func TestParseFunctionParameterWithLabel(t *testing.T) { + + t.Parallel() + + result, errs := testParseDeclarations(` + fun test(x y: Int) { } + `) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.FunctionDeclaration{ + Access: ast.AccessNotSpecified, + Identifier: ast.Identifier{ + Identifier: "test", + Pos: ast.Position{Offset: 10, Line: 2, Column: 9}, + }, + ParameterList: &ast.ParameterList{ + Parameters: []*ast.Parameter{ + { + Label: "x", + Identifier: ast.Identifier{ + Identifier: "y", + Pos: ast.Position{Offset: 17, Line: 2, Column: 16}, + }, + TypeAnnotation: &ast.TypeAnnotation{ + IsResource: false, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Int", + Pos: ast.Position{Offset: 20, Line: 2, Column: 19}, + }, + }, + StartPos: ast.Position{Offset: 20, Line: 2, Column: 19}, + }, + StartPos: ast.Position{Offset: 15, Line: 2, Column: 14}, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 14, Line: 2, Column: 13}, + EndPos: ast.Position{Offset: 23, Line: 2, Column: 22}, + }, + }, + FunctionBlock: &ast.FunctionBlock{ + Block: &ast.Block{ + Range: ast.Range{ + StartPos: ast.Position{Offset: 25, Line: 2, Column: 24}, + EndPos: ast.Position{Offset: 27, Line: 2, Column: 26}, + }, + }, + }, + StartPos: ast.Position{Offset: 6, Line: 2, Column: 5}, + }, + }, + result, + ) +} + +func TestParseStructure(t *testing.T) { + + t.Parallel() + + const code = ` + struct Test { + pub(set) var foo: Int + + init(foo: Int) { + self.foo = foo + } + + pub fun getFoo(): Int { + return self.foo + } + } + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.CompositeDeclaration{ + Access: ast.AccessNotSpecified, + CompositeKind: common.CompositeKindStructure, + Identifier: ast.Identifier{ + Identifier: "Test", + Pos: ast.Position{Offset: 16, Line: 2, Column: 15}, + }, + Members: ast.NewUnmeteredMembers( + []ast.Declaration{ + &ast.FieldDeclaration{ + Access: ast.PubSettableLegacy, + VariableKind: ast.VariableKindVariable, + Identifier: ast.Identifier{ + Identifier: "foo", + Pos: ast.Position{Offset: 48, Line: 3, Column: 25}, + }, + TypeAnnotation: &ast.TypeAnnotation{ + IsResource: false, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Int", + Pos: ast.Position{Offset: 53, Line: 3, Column: 30}, + }, + }, + StartPos: ast.Position{Offset: 53, Line: 3, Column: 30}, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 35, Line: 3, Column: 12}, + EndPos: ast.Position{Offset: 55, Line: 3, Column: 32}, + }, + }, + &ast.SpecialFunctionDeclaration{ + Kind: common.DeclarationKindInitializer, + FunctionDeclaration: &ast.FunctionDeclaration{ + Access: ast.AccessNotSpecified, + Identifier: ast.Identifier{ + Identifier: "init", + Pos: ast.Position{Offset: 70, Line: 5, Column: 12}, + }, + ParameterList: &ast.ParameterList{ + Parameters: []*ast.Parameter{ + { + Label: "", + Identifier: ast.Identifier{ + Identifier: "foo", + Pos: ast.Position{Offset: 75, Line: 5, Column: 17}, + }, + TypeAnnotation: &ast.TypeAnnotation{ + IsResource: false, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Int", + Pos: ast.Position{Offset: 80, Line: 5, Column: 22}, + }, + }, + StartPos: ast.Position{Offset: 80, Line: 5, Column: 22}, + }, + StartPos: ast.Position{Offset: 75, Line: 5, Column: 17}, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 74, Line: 5, Column: 16}, + EndPos: ast.Position{Offset: 83, Line: 5, Column: 25}, + }, + }, + FunctionBlock: &ast.FunctionBlock{ + Block: &ast.Block{ + Statements: []ast.Statement{ + &ast.AssignmentStatement{ + Target: &ast.MemberExpression{ + Expression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "self", + Pos: ast.Position{Offset: 103, Line: 6, Column: 16}, + }, + }, + AccessPos: ast.Position{Offset: 107, Line: 6, Column: 20}, + Identifier: ast.Identifier{ + Identifier: "foo", + Pos: ast.Position{Offset: 108, Line: 6, Column: 21}, + }, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationCopy, + Pos: ast.Position{Offset: 112, Line: 6, Column: 25}, + }, + Value: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "foo", + Pos: ast.Position{Offset: 114, Line: 6, Column: 27}, + }, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 85, Line: 5, Column: 27}, + EndPos: ast.Position{Offset: 130, Line: 7, Column: 12}, + }, + }, + }, + StartPos: ast.Position{Offset: 70, Line: 5, Column: 12}, + }, + }, + &ast.FunctionDeclaration{ + Access: ast.AccessAll, + Identifier: ast.Identifier{ + Identifier: "getFoo", + Pos: ast.Position{Offset: 153, Line: 9, Column: 20}, + }, + ParameterList: &ast.ParameterList{ + Range: ast.Range{ + StartPos: ast.Position{Offset: 159, Line: 9, Column: 26}, + EndPos: ast.Position{Offset: 160, Line: 9, Column: 27}, + }, + }, + ReturnTypeAnnotation: &ast.TypeAnnotation{ + IsResource: false, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Int", + Pos: ast.Position{Offset: 163, Line: 9, Column: 30}, + }, + }, + StartPos: ast.Position{Offset: 163, Line: 9, Column: 30}, + }, + FunctionBlock: &ast.FunctionBlock{ + Block: &ast.Block{ + Statements: []ast.Statement{ + &ast.ReturnStatement{ + Expression: &ast.MemberExpression{ + Expression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "self", + Pos: ast.Position{Offset: 192, Line: 10, Column: 23}, + }, + }, + AccessPos: ast.Position{Offset: 196, Line: 10, Column: 27}, + Identifier: ast.Identifier{ + Identifier: "foo", + Pos: ast.Position{Offset: 197, Line: 10, Column: 28}, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 185, Line: 10, Column: 16}, + EndPos: ast.Position{Offset: 199, Line: 10, Column: 30}, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 167, Line: 9, Column: 34}, + EndPos: ast.Position{Offset: 213, Line: 11, Column: 12}, + }, + }, + }, + StartPos: ast.Position{Offset: 145, Line: 9, Column: 12}, + }, + }, + ), + Range: ast.Range{ + StartPos: ast.Position{Offset: 9, Line: 2, Column: 8}, + EndPos: ast.Position{Offset: 223, Line: 12, Column: 8}, + }, + }, + }, + result.Declarations(), + ) +} + +func TestParseStructureWithConformances(t *testing.T) { + + t.Parallel() + + const code = ` + struct Test: Foo, Bar {} + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.CompositeDeclaration{ + Access: ast.AccessNotSpecified, + CompositeKind: common.CompositeKindStructure, + Identifier: ast.Identifier{ + Identifier: "Test", + Pos: ast.Position{Offset: 16, Line: 2, Column: 15}, + }, + Conformances: []*ast.NominalType{ + { + Identifier: ast.Identifier{ + Identifier: "Foo", + Pos: ast.Position{Offset: 22, Line: 2, Column: 21}, + }, + }, + { + Identifier: ast.Identifier{ + Identifier: "Bar", + Pos: ast.Position{Offset: 27, Line: 2, Column: 26}, + }, + }, + }, + Members: &ast.Members{}, + Range: ast.Range{ + StartPos: ast.Position{Offset: 9, Line: 2, Column: 8}, + EndPos: ast.Position{Offset: 32, Line: 2, Column: 31}, + }, + }, + }, + result.Declarations(), + ) +} + +func TestParseInvalidMember(t *testing.T) { + + // For now, leading unknown identifiers are valid. + // This will be rejected in Stable Cadence. + + t.Parallel() + + const code = ` + struct Test { + foo let x: Int + } + ` + + _, errs := testParseDeclarations(code) + + require.Empty(t, errs) +} + +func TestParsePreAndPostConditions(t *testing.T) { + + t.Parallel() + + const code = ` + fun test(n: Int) { + pre { + n != 0 + n > 0 + } + post { + result == 0 + } + return 0 + } + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.FunctionDeclaration{ + Access: ast.AccessNotSpecified, + Identifier: ast.Identifier{ + Identifier: "test", + Pos: ast.Position{Offset: 13, Line: 2, Column: 12}, + }, + ParameterList: &ast.ParameterList{ + Parameters: []*ast.Parameter{ + { + Label: "", + Identifier: ast.Identifier{ + Identifier: "n", + Pos: ast.Position{Offset: 18, Line: 2, Column: 17}, + }, + TypeAnnotation: &ast.TypeAnnotation{ + IsResource: false, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Int", + Pos: ast.Position{Offset: 21, Line: 2, Column: 20}, + }, + }, + StartPos: ast.Position{Offset: 21, Line: 2, Column: 20}, + }, + StartPos: ast.Position{Offset: 18, Line: 2, Column: 17}, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 17, Line: 2, Column: 16}, + EndPos: ast.Position{Offset: 24, Line: 2, Column: 23}, + }, + }, + FunctionBlock: &ast.FunctionBlock{ + Block: &ast.Block{ + Statements: []ast.Statement{ + &ast.ReturnStatement{ + Expression: &ast.IntegerExpression{ + PositiveLiteral: []byte("0"), + Value: new(big.Int), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 185, Line: 10, Column: 19}, + EndPos: ast.Position{Offset: 185, Line: 10, Column: 19}, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 178, Line: 10, Column: 12}, + EndPos: ast.Position{Offset: 185, Line: 10, Column: 19}, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 26, Line: 2, Column: 25}, + EndPos: ast.Position{Offset: 195, Line: 11, Column: 8}, + }, + }, + PreConditions: &ast.Conditions{ + &ast.TestCondition{ + Test: &ast.BinaryExpression{ + Operation: ast.OperationNotEqual, + Left: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "n", + Pos: ast.Position{Offset: 62, Line: 4, Column: 16}, + }, + }, + Right: &ast.IntegerExpression{ + PositiveLiteral: []byte("0"), + Value: new(big.Int), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 67, Line: 4, Column: 21}, + EndPos: ast.Position{Offset: 67, Line: 4, Column: 21}, + }, + }, + }, + }, + &ast.TestCondition{ + Test: &ast.BinaryExpression{ + Operation: ast.OperationGreater, + Left: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "n", + Pos: ast.Position{Offset: 85, Line: 5, Column: 16}, + }, + }, + Right: &ast.IntegerExpression{ + PositiveLiteral: []byte("0"), + Value: new(big.Int), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 89, Line: 5, Column: 20}, + EndPos: ast.Position{Offset: 89, Line: 5, Column: 20}, + }, + }, + }, + }, + }, + PostConditions: &ast.Conditions{ + &ast.TestCondition{ + Test: &ast.BinaryExpression{ + Operation: ast.OperationEqual, + Left: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "result", + Pos: ast.Position{Offset: 140, Line: 8, Column: 16}, + }, + }, + Right: &ast.IntegerExpression{ + PositiveLiteral: []byte("0"), + Value: new(big.Int), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 150, Line: 8, Column: 26}, + EndPos: ast.Position{Offset: 150, Line: 8, Column: 26}, + }, + }, + }, + }, + }, + }, + StartPos: ast.Position{Offset: 9, Line: 2, Column: 8}, + }, + }, + result.Declarations(), + ) +} + +func TestParseConditionMessage(t *testing.T) { + + t.Parallel() + + const code = ` + fun test(n: Int) { + pre { + n >= 0: "n must be positive" + } + return n + } + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.FunctionDeclaration{ + Access: ast.AccessNotSpecified, + Identifier: ast.Identifier{ + Identifier: "test", + Pos: ast.Position{Offset: 13, Line: 2, Column: 12}, + }, + ParameterList: &ast.ParameterList{ + Parameters: []*ast.Parameter{ + { + Label: "", + Identifier: ast.Identifier{Identifier: "n", + Pos: ast.Position{Offset: 18, Line: 2, Column: 17}, + }, + TypeAnnotation: &ast.TypeAnnotation{ + IsResource: false, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Int", + Pos: ast.Position{Offset: 21, Line: 2, Column: 20}, + }, + }, + StartPos: ast.Position{Offset: 21, Line: 2, Column: 20}, + }, + StartPos: ast.Position{Offset: 18, Line: 2, Column: 17}, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 17, Line: 2, Column: 16}, + EndPos: ast.Position{Offset: 24, Line: 2, Column: 23}, + }, + }, + FunctionBlock: &ast.FunctionBlock{ + Block: &ast.Block{ + Statements: []ast.Statement{ + &ast.ReturnStatement{ + Expression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "n", + Pos: ast.Position{Offset: 124, Line: 6, Column: 19}, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 117, Line: 6, Column: 12}, + EndPos: ast.Position{Offset: 124, Line: 6, Column: 19}, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 26, Line: 2, Column: 25}, + EndPos: ast.Position{Offset: 134, Line: 7, Column: 8}, + }, + }, + PreConditions: &ast.Conditions{ + &ast.TestCondition{ + Test: &ast.BinaryExpression{ + Operation: ast.OperationGreaterEqual, + Left: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "n", + Pos: ast.Position{Offset: 62, Line: 4, Column: 16}, + }, + }, + Right: &ast.IntegerExpression{ + PositiveLiteral: []byte("0"), + Value: new(big.Int), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 67, Line: 4, Column: 21}, + EndPos: ast.Position{Offset: 67, Line: 4, Column: 21}, + }, + }, + }, + Message: &ast.StringExpression{ + Value: "n must be positive", + Range: ast.Range{ + StartPos: ast.Position{Offset: 70, Line: 4, Column: 24}, + EndPos: ast.Position{Offset: 89, Line: 4, Column: 43}, + }, + }, + }, + }, + PostConditions: nil, + }, + StartPos: ast.Position{Offset: 9, Line: 2, Column: 8}, + }, + }, + result.Declarations(), + ) +} + +func TestParseInterface(t *testing.T) { + + t.Parallel() + + for _, kind := range common.InstantiableCompositeKindsWithFieldsAndFunctions { + + code := fmt.Sprintf(` + %s interface Test { + foo: Int + + init(foo: Int) + + fun getFoo(): Int + } + `, kind.Keyword()) + actual, err := testParseProgram(code) + + require.NoError(t, err) + + // only compare AST for one kind: structs + + if kind != common.CompositeKindStructure { + continue + } + + test := &ast.InterfaceDeclaration{ + Access: ast.AccessNotSpecified, + CompositeKind: common.CompositeKindStructure, + Identifier: ast.Identifier{ + Identifier: "Test", + Pos: ast.Position{Offset: 30, Line: 2, Column: 29}, + }, + Members: ast.NewUnmeteredMembers( + []ast.Declaration{ + &ast.FieldDeclaration{ + Access: ast.AccessNotSpecified, + VariableKind: ast.VariableKindNotSpecified, + Identifier: ast.Identifier{ + Identifier: "foo", + Pos: ast.Position{Offset: 53, Line: 3, Column: 16}, + }, + TypeAnnotation: &ast.TypeAnnotation{ + IsResource: false, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Int", + Pos: ast.Position{Offset: 58, Line: 3, Column: 21}, + }, + }, + StartPos: ast.Position{Offset: 58, Line: 3, Column: 21}, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 53, Line: 3, Column: 16}, + EndPos: ast.Position{Offset: 60, Line: 3, Column: 23}, + }, + }, + &ast.SpecialFunctionDeclaration{ + Kind: common.DeclarationKindInitializer, + FunctionDeclaration: &ast.FunctionDeclaration{ + Access: ast.AccessNotSpecified, + Identifier: ast.Identifier{ + Identifier: "init", + Pos: ast.Position{Offset: 79, Line: 5, Column: 16}, + }, + ParameterList: &ast.ParameterList{ + Parameters: []*ast.Parameter{ + { + Label: "", + Identifier: ast.Identifier{ + Identifier: "foo", + Pos: ast.Position{Offset: 84, Line: 5, Column: 21}, + }, + TypeAnnotation: &ast.TypeAnnotation{ + IsResource: false, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Int", + Pos: ast.Position{Offset: 89, Line: 5, Column: 26}, + }, + }, + StartPos: ast.Position{Offset: 89, Line: 5, Column: 26}, + }, + StartPos: ast.Position{Offset: 84, Line: 5, Column: 21}, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 83, Line: 5, Column: 20}, + EndPos: ast.Position{Offset: 92, Line: 5, Column: 29}, + }, + }, + FunctionBlock: nil, + StartPos: ast.Position{Offset: 79, Line: 5, Column: 16}, + }, + }, + &ast.FunctionDeclaration{ + Access: ast.AccessNotSpecified, + Identifier: ast.Identifier{ + Identifier: "getFoo", + Pos: ast.Position{Offset: 115, Line: 7, Column: 20}, + }, + ParameterList: &ast.ParameterList{ + Range: ast.Range{ + StartPos: ast.Position{Offset: 121, Line: 7, Column: 26}, + EndPos: ast.Position{Offset: 122, Line: 7, Column: 27}, + }, + }, + ReturnTypeAnnotation: &ast.TypeAnnotation{ + IsResource: false, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Int", + Pos: ast.Position{Offset: 125, Line: 7, Column: 30}, + }, + }, + StartPos: ast.Position{Offset: 125, Line: 7, Column: 30}, + }, + FunctionBlock: nil, + StartPos: ast.Position{Offset: 111, Line: 7, Column: 16}, + }, + }, + ), + Range: ast.Range{ + StartPos: ast.Position{Offset: 13, Line: 2, Column: 12}, + EndPos: ast.Position{Offset: 141, Line: 8, Column: 12}, + }, + } + + utils.AssertEqualWithDiff(t, + []ast.Declaration{test}, + actual.Declarations(), + ) + } +} + +func TestParsePragmaNoArguments(t *testing.T) { + + t.Parallel() + + t.Run("identifier", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseDeclarations(`#pedantic`) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.PragmaDeclaration{ + Expression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "pedantic", + Pos: ast.Position{Offset: 1, Line: 1, Column: 1}, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 0, Line: 1, Column: 0}, + EndPos: ast.Position{Offset: 8, Line: 1, Column: 8}, + }, + }, + }, + result, + ) + }) + + t.Run("static, enabled", func(t *testing.T) { + + t.Parallel() + + _, errs := ParseDeclarations( + nil, + []byte("static #foo"), + Config{ + StaticModifierEnabled: true, + }, + ) + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "invalid static modifier for pragma", + Pos: ast.Position{Offset: 0, Line: 1, Column: 0}, + }, + }, + errs, + ) + }) + + t.Run("static, disabled", func(t *testing.T) { + + t.Parallel() + + _, errs := testParseDeclarations("static #foo") + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "unexpected token: identifier", + Pos: ast.Position{Offset: 0, Line: 1, Column: 0}, + }, + }, + errs, + ) + }) + + t.Run("native, enabled", func(t *testing.T) { + + t.Parallel() + + _, errs := ParseDeclarations( + nil, + []byte("native #foo"), + Config{ + NativeModifierEnabled: true, + }, + ) + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "invalid native modifier for pragma", + Pos: ast.Position{Offset: 0, Line: 1, Column: 0}, + }, + }, + errs, + ) + }) + + t.Run("native, disabled", func(t *testing.T) { + + t.Parallel() + + _, errs := testParseDeclarations("native #foo") + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "unexpected token: identifier", + Pos: ast.Position{Offset: 0, Line: 1, Column: 0}, + }, + }, + errs, + ) + }) +} + +func TestParsePragmaArguments(t *testing.T) { + + t.Parallel() + + const code = `#version("1.0")` + actual, err := testParseProgram(code) + require.NoError(t, err) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.PragmaDeclaration{ + Expression: &ast.InvocationExpression{ + InvokedExpression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "version", + Pos: ast.Position{Offset: 1, Line: 1, Column: 1}, + }, + }, + Arguments: ast.Arguments{ + { + Expression: &ast.StringExpression{ + Value: "1.0", + Range: ast.Range{ + StartPos: ast.Position{Offset: 9, Line: 1, Column: 9}, + EndPos: ast.Position{Offset: 13, Line: 1, Column: 13}, + }, + }, + TrailingSeparatorPos: ast.Position{Offset: 14, Line: 1, Column: 14}, + }, + }, + ArgumentsStartPos: ast.Position{Offset: 8, Line: 1, Column: 8}, + EndPos: ast.Position{Offset: 14, Line: 1, Column: 14}, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 0, Line: 1, Column: 0}, + EndPos: ast.Position{Offset: 14, Line: 1, Column: 14}, + }, + }, + }, + actual.Declarations(), + ) +} + +func TestParseImportWithString(t *testing.T) { + + t.Parallel() + + const code = ` + import "test.cdc" + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.ImportDeclaration{ + Identifiers: nil, + Location: common.StringLocation("test.cdc"), + Range: ast.Range{ + StartPos: ast.Position{Offset: 9, Line: 2, Column: 8}, + EndPos: ast.Position{Offset: 25, Line: 2, Column: 24}, + }, + LocationPos: ast.Position{Offset: 16, Line: 2, Column: 15}, + }, + }, + result.Declarations(), + ) +} + +func TestParseImportWithAddress(t *testing.T) { + + t.Parallel() + + const code = ` + import 0x1234 + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.ImportDeclaration{ + Identifiers: nil, + Location: common.AddressLocation{ + Address: common.MustBytesToAddress([]byte{0x12, 0x34}), + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 9, Line: 2, Column: 8}, + EndPos: ast.Position{Offset: 21, Line: 2, Column: 20}, + }, + LocationPos: ast.Position{Offset: 16, Line: 2, Column: 15}, + }, + }, + result.Declarations(), + ) +} + +func TestParseImportWithIdentifiers(t *testing.T) { + + t.Parallel() + + const code = ` + import A, b from 0x1 + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.ImportDeclaration{ + Identifiers: []ast.Identifier{ + { + Identifier: "A", + Pos: ast.Position{Offset: 16, Line: 2, Column: 15}, + }, + { + Identifier: "b", + Pos: ast.Position{Offset: 19, Line: 2, Column: 18}, + }, + }, + Location: common.AddressLocation{ + Address: common.MustBytesToAddress([]byte{0x1}), + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 9, Line: 2, Column: 8}, + EndPos: ast.Position{Offset: 28, Line: 2, Column: 27}, + }, + LocationPos: ast.Position{Offset: 26, Line: 2, Column: 25}, + }, + }, + result.Declarations(), + ) +} + +func TestParseFieldWithFromIdentifier(t *testing.T) { + + t.Parallel() + + const code = ` + struct S { + let from: String + } + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.CompositeDeclaration{ + Access: ast.AccessNotSpecified, + CompositeKind: common.CompositeKindStructure, + Identifier: ast.Identifier{ + Identifier: "S", + Pos: ast.Position{Offset: 14, Line: 2, Column: 13}, + }, + Members: ast.NewUnmeteredMembers( + []ast.Declaration{ + &ast.FieldDeclaration{ + Access: ast.AccessNotSpecified, + VariableKind: ast.VariableKindConstant, + Identifier: ast.Identifier{ + Identifier: "from", + Pos: ast.Position{Offset: 32, Line: 3, Column: 14}, + }, + TypeAnnotation: &ast.TypeAnnotation{ + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "String", + Pos: ast.Position{Offset: 38, Line: 3, Column: 20}, + }, + }, + StartPos: ast.Position{Offset: 38, Line: 3, Column: 20}, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 28, Line: 3, Column: 10}, + EndPos: ast.Position{Offset: 43, Line: 3, Column: 25}, + }, + }, + }, + ), + Range: ast.Range{ + StartPos: ast.Position{Offset: 7, Line: 2, Column: 6}, + EndPos: ast.Position{Offset: 51, Line: 4, Column: 6}, + }, + }, + }, + result.Declarations(), + ) +} + +func TestParseFunctionWithFromIdentifier(t *testing.T) { + + t.Parallel() + + const code = ` + fun send(from: String, to: String) {} + ` + _, errs := testParseProgram(code) + require.Empty(t, errs) +} + +func TestParseImportWithFromIdentifier(t *testing.T) { + + t.Parallel() + + const code = ` + import from from 0x1 + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.ImportDeclaration{ + Identifiers: []ast.Identifier{ + { + Identifier: "from", + Pos: ast.Position{Offset: 16, Line: 2, Column: 15}, + }, + }, + Location: common.AddressLocation{ + Address: common.MustBytesToAddress([]byte{0x1}), + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 9, Line: 2, Column: 8}, + EndPos: ast.Position{Offset: 28, Line: 2, Column: 27}, + }, + LocationPos: ast.Position{Offset: 26, Line: 2, Column: 25}, + }, + }, + result.Declarations(), + ) +} + +func TestParseSemicolonsBetweenDeclarations(t *testing.T) { + + t.Parallel() + + const code = ` + import from from 0x0; + fun foo() {}; + ` + _, errs := testParseProgram(code) + require.Empty(t, errs) +} + +func TestParseResource(t *testing.T) { + + t.Parallel() + + const code = ` + resource Test {} + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.CompositeDeclaration{ + Access: ast.AccessNotSpecified, + CompositeKind: common.CompositeKindResource, + Identifier: ast.Identifier{ + Identifier: "Test", + Pos: ast.Position{Offset: 18, Line: 2, Column: 17}, + }, + Members: &ast.Members{}, + Range: ast.Range{ + StartPos: ast.Position{Offset: 9, Line: 2, Column: 8}, + EndPos: ast.Position{Offset: 24, Line: 2, Column: 23}, + }, + }, + }, + result.Declarations(), + ) +} + +func TestParseEventDeclaration(t *testing.T) { + + t.Parallel() + + const code = ` + event Transfer(to: Address, from: Address) + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.CompositeDeclaration{ + Access: ast.AccessNotSpecified, + CompositeKind: common.CompositeKindEvent, + Identifier: ast.Identifier{ + Identifier: "Transfer", + Pos: ast.Position{Offset: 15, Line: 2, Column: 14}, + }, + Members: ast.NewUnmeteredMembers( + []ast.Declaration{ + &ast.SpecialFunctionDeclaration{ + Kind: common.DeclarationKindInitializer, + FunctionDeclaration: &ast.FunctionDeclaration{ + Access: ast.AccessNotSpecified, + ParameterList: &ast.ParameterList{ + Parameters: []*ast.Parameter{ + { + Label: "", + Identifier: ast.Identifier{ + Identifier: "to", + Pos: ast.Position{Offset: 24, Line: 2, Column: 23}, + }, + TypeAnnotation: &ast.TypeAnnotation{ + IsResource: false, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Address", + Pos: ast.Position{Offset: 28, Line: 2, Column: 27}, + }, + }, + StartPos: ast.Position{Offset: 28, Line: 2, Column: 27}, + }, + StartPos: ast.Position{Offset: 24, Line: 2, Column: 23}, + }, + { + Label: "", + Identifier: ast.Identifier{ + Identifier: "from", + Pos: ast.Position{Offset: 37, Line: 2, Column: 36}, + }, + TypeAnnotation: &ast.TypeAnnotation{ + IsResource: false, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Address", + Pos: ast.Position{Offset: 43, Line: 2, Column: 42}, + }, + }, + StartPos: ast.Position{Offset: 43, Line: 2, Column: 42}, + }, + StartPos: ast.Position{Offset: 37, Line: 2, Column: 36}, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 23, Line: 2, Column: 22}, + EndPos: ast.Position{Offset: 50, Line: 2, Column: 49}, + }, + }, + StartPos: ast.Position{Offset: 23, Line: 2, Column: 22}, + }, + }, + }, + ), + Range: ast.Range{ + StartPos: ast.Position{Offset: 9, Line: 2, Column: 8}, + EndPos: ast.Position{Offset: 50, Line: 2, Column: 49}, + }, + }, + }, + result.Declarations(), + ) +} + +func TestParseEventEmitStatement(t *testing.T) { + + t.Parallel() + + const code = ` + fun test() { + emit Transfer(to: 1, from: 2) + } + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.FunctionDeclaration{ + Access: ast.AccessNotSpecified, + Identifier: ast.Identifier{ + Identifier: "test", + Pos: ast.Position{Offset: 11, Line: 2, Column: 10}, + }, + ParameterList: &ast.ParameterList{ + Range: ast.Range{ + StartPos: ast.Position{Offset: 15, Line: 2, Column: 14}, + EndPos: ast.Position{Offset: 16, Line: 2, Column: 15}, + }, + }, + FunctionBlock: &ast.FunctionBlock{ + Block: &ast.Block{ + Statements: []ast.Statement{ + &ast.EmitStatement{ + InvocationExpression: &ast.InvocationExpression{ + InvokedExpression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "Transfer", + Pos: ast.Position{Offset: 33, Line: 3, Column: 13}, + }, + }, + Arguments: ast.Arguments{ + { + Label: "to", + LabelStartPos: &ast.Position{Offset: 42, Line: 3, Column: 22}, + LabelEndPos: &ast.Position{Offset: 44, Line: 3, Column: 24}, + Expression: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 46, Line: 3, Column: 26}, + EndPos: ast.Position{Offset: 46, Line: 3, Column: 26}, + }, + }, + TrailingSeparatorPos: ast.Position{Offset: 47, Line: 3, Column: 27}, + }, + { + Label: "from", + LabelStartPos: &ast.Position{Offset: 49, Line: 3, Column: 29}, + LabelEndPos: &ast.Position{Offset: 53, Line: 3, Column: 33}, + Expression: &ast.IntegerExpression{ + PositiveLiteral: []byte("2"), + Value: big.NewInt(2), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 55, Line: 3, Column: 35}, + EndPos: ast.Position{Offset: 55, Line: 3, Column: 35}, + }, + }, + TrailingSeparatorPos: ast.Position{Offset: 56, Line: 3, Column: 36}, + }, + }, + ArgumentsStartPos: ast.Position{Offset: 41, Line: 3, Column: 21}, + EndPos: ast.Position{Offset: 56, Line: 3, Column: 36}, + }, + StartPos: ast.Position{Offset: 28, Line: 3, Column: 8}, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 18, Line: 2, Column: 17}, + EndPos: ast.Position{Offset: 64, Line: 4, Column: 6}, + }, + }, + }, + StartPos: ast.Position{Offset: 7, Line: 2, Column: 6}, + }, + }, + result.Declarations(), + ) +} + +func TestParseResourceReturnType(t *testing.T) { + + t.Parallel() + + const code = ` + fun test(): @X {} + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.FunctionDeclaration{ + Access: ast.AccessNotSpecified, + Identifier: ast.Identifier{ + Identifier: "test", + Pos: ast.Position{Offset: 13, Line: 2, Column: 12}, + }, + ParameterList: &ast.ParameterList{ + Range: ast.Range{ + StartPos: ast.Position{Offset: 17, Line: 2, Column: 16}, + EndPos: ast.Position{Offset: 18, Line: 2, Column: 17}, + }, + }, + ReturnTypeAnnotation: &ast.TypeAnnotation{ + IsResource: true, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "X", + Pos: ast.Position{Offset: 22, Line: 2, Column: 21}, + }, + }, + StartPos: ast.Position{Offset: 21, Line: 2, Column: 20}, + }, + FunctionBlock: &ast.FunctionBlock{ + Block: &ast.Block{ + Range: ast.Range{ + StartPos: ast.Position{Offset: 24, Line: 2, Column: 23}, + EndPos: ast.Position{Offset: 25, Line: 2, Column: 24}, + }, + }, + }, + StartPos: ast.Position{Offset: 9, Line: 2, Column: 8}, + }, + }, + result.Declarations(), + ) +} + +func TestParseMovingVariableDeclaration(t *testing.T) { + + t.Parallel() + + const code = ` + let x <- y + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.VariableDeclaration{ + Access: ast.AccessNotSpecified, + IsConstant: true, + Identifier: ast.Identifier{ + Identifier: "x", + Pos: ast.Position{Offset: 13, Line: 2, Column: 12}, + }, + Value: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "y", + Pos: ast.Position{Offset: 18, Line: 2, Column: 17}, + }, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationMove, + Pos: ast.Position{Offset: 15, Line: 2, Column: 14}, + }, + StartPos: ast.Position{Offset: 9, Line: 2, Column: 8}, + }, + }, + result.Declarations(), + ) +} + +func TestParseResourceParameterType(t *testing.T) { + + t.Parallel() + + const code = ` + fun test(x: @X) {} + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.FunctionDeclaration{ + Access: ast.AccessNotSpecified, + Identifier: ast.Identifier{ + Identifier: "test", + Pos: ast.Position{Offset: 13, Line: 2, Column: 12}, + }, + ParameterList: &ast.ParameterList{ + Parameters: []*ast.Parameter{ + { + Label: "", + Identifier: ast.Identifier{ + Identifier: "x", + Pos: ast.Position{Offset: 18, Line: 2, Column: 17}, + }, + TypeAnnotation: &ast.TypeAnnotation{ + IsResource: true, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "X", + Pos: ast.Position{Offset: 22, Line: 2, Column: 21}, + }, + }, + StartPos: ast.Position{Offset: 21, Line: 2, Column: 20}, + }, + StartPos: ast.Position{Offset: 18, Line: 2, Column: 17}, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 17, Line: 2, Column: 16}, + EndPos: ast.Position{Offset: 23, Line: 2, Column: 22}, + }, + }, + FunctionBlock: &ast.FunctionBlock{ + Block: &ast.Block{ + Range: ast.Range{ + StartPos: ast.Position{Offset: 25, Line: 2, Column: 24}, + EndPos: ast.Position{Offset: 26, Line: 2, Column: 25}, + }, + }, + }, + StartPos: ast.Position{Offset: 9, Line: 2, Column: 8}, + }, + }, + result.Declarations(), + ) +} + +func TestParseMovingVariableDeclarationWithTypeAnnotation(t *testing.T) { + + t.Parallel() + + const code = ` + let x: @R <- y + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.VariableDeclaration{ + Access: ast.AccessNotSpecified, + IsConstant: true, + Identifier: ast.Identifier{ + Identifier: "x", + Pos: ast.Position{Offset: 13, Line: 2, Column: 12}, + }, + TypeAnnotation: &ast.TypeAnnotation{ + IsResource: true, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "R", + Pos: ast.Position{Offset: 17, Line: 2, Column: 16}, + }, + }, + StartPos: ast.Position{Offset: 16, Line: 2, Column: 15}, + }, + Value: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "y", + Pos: ast.Position{Offset: 22, Line: 2, Column: 21}, + }, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationMove, + Pos: ast.Position{Offset: 19, Line: 2, Column: 18}, + }, + StartPos: ast.Position{Offset: 9, Line: 2, Column: 8}, + }, + }, + result.Declarations(), + ) +} + +func TestParseFieldDeclarationWithMoveTypeAnnotation(t *testing.T) { + + t.Parallel() + + const code = ` + struct X { x: @R } + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.CompositeDeclaration{ + Access: ast.AccessNotSpecified, + CompositeKind: common.CompositeKindStructure, + Identifier: ast.Identifier{ + Identifier: "X", + Pos: ast.Position{Offset: 16, Line: 2, Column: 15}, + }, + Members: ast.NewUnmeteredMembers( + []ast.Declaration{ + &ast.FieldDeclaration{ + Access: ast.AccessNotSpecified, + VariableKind: ast.VariableKindNotSpecified, + Identifier: ast.Identifier{ + Identifier: "x", + Pos: ast.Position{Offset: 20, Line: 2, Column: 19}, + }, + TypeAnnotation: &ast.TypeAnnotation{ + IsResource: true, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "R", + Pos: ast.Position{Offset: 24, Line: 2, Column: 23}, + }, + }, + StartPos: ast.Position{Offset: 23, Line: 2, Column: 22}, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 20, Line: 2, Column: 19}, + EndPos: ast.Position{Offset: 24, Line: 2, Column: 23}, + }, + }, + }, + ), + Range: ast.Range{ + StartPos: ast.Position{Offset: 9, Line: 2, Column: 8}, + EndPos: ast.Position{Offset: 26, Line: 2, Column: 25}, + }, + }, + }, + result.Declarations(), + ) +} + +func TestParseDestructor(t *testing.T) { + + t.Parallel() + + const code = ` + resource Test { + destroy() {} + } + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.CompositeDeclaration{ + Access: ast.AccessNotSpecified, + CompositeKind: common.CompositeKindResource, + Identifier: ast.Identifier{ + Identifier: "Test", + Pos: ast.Position{Offset: 18, Line: 2, Column: 17}, + }, + Members: ast.NewUnmeteredMembers( + []ast.Declaration{ + &ast.SpecialFunctionDeclaration{ + Kind: common.DeclarationKindDestructorLegacy, + FunctionDeclaration: &ast.FunctionDeclaration{ + Access: ast.AccessNotSpecified, + Identifier: ast.Identifier{ + Identifier: "destroy", + Pos: ast.Position{Offset: 37, Line: 3, Column: 12}, + }, + ParameterList: &ast.ParameterList{ + Range: ast.Range{ + StartPos: ast.Position{Offset: 44, Line: 3, Column: 19}, + EndPos: ast.Position{Offset: 45, Line: 3, Column: 20}, + }, + }, + FunctionBlock: &ast.FunctionBlock{ + Block: &ast.Block{ + Range: ast.Range{ + StartPos: ast.Position{Offset: 47, Line: 3, Column: 22}, + EndPos: ast.Position{Offset: 48, Line: 3, Column: 23}, + }, + }, + }, + StartPos: ast.Position{Offset: 37, Line: 3, Column: 12}, + }, + }, + }, + ), + Range: ast.Range{ + StartPos: ast.Position{Offset: 9, Line: 2, Column: 8}, + EndPos: ast.Position{Offset: 58, Line: 4, Column: 8}, + }, + }, + }, + result.Declarations(), + ) +} + +func TestParseCompositeDeclarationWithSemicolonSeparatedMembers(t *testing.T) { + + t.Parallel() + + const code = ` + struct Kitty { let id: Int ; init(id: Int) { self.id = id } } + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.CompositeDeclaration{ + Access: ast.AccessNotSpecified, + CompositeKind: common.CompositeKindStructure, + Identifier: ast.Identifier{ + Identifier: "Kitty", + Pos: ast.Position{Offset: 16, Line: 2, Column: 15}, + }, + Members: ast.NewUnmeteredMembers( + []ast.Declaration{ + &ast.FieldDeclaration{ + Access: ast.AccessNotSpecified, + VariableKind: ast.VariableKindConstant, + Identifier: ast.Identifier{ + Identifier: "id", + Pos: ast.Position{Offset: 28, Line: 2, Column: 27}, + }, + TypeAnnotation: &ast.TypeAnnotation{ + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Int", + Pos: ast.Position{Offset: 32, Line: 2, Column: 31}, + }, + }, + StartPos: ast.Position{Offset: 32, Line: 2, Column: 31}, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 24, Line: 2, Column: 23}, + EndPos: ast.Position{Offset: 34, Line: 2, Column: 33}, + }, + }, + &ast.SpecialFunctionDeclaration{ + Kind: common.DeclarationKindInitializer, + FunctionDeclaration: &ast.FunctionDeclaration{ + Access: ast.AccessNotSpecified, + Identifier: ast.Identifier{ + Identifier: "init", + Pos: ast.Position{Offset: 38, Line: 2, Column: 37}, + }, + ParameterList: &ast.ParameterList{ + Parameters: []*ast.Parameter{ + { + Identifier: ast.Identifier{ + Identifier: "id", + Pos: ast.Position{Offset: 43, Line: 2, Column: 42}, + }, + TypeAnnotation: &ast.TypeAnnotation{ + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Int", + Pos: ast.Position{Offset: 47, Line: 2, Column: 46}, + }, + }, + StartPos: ast.Position{Offset: 47, Line: 2, Column: 46}, + }, + StartPos: ast.Position{Offset: 43, Line: 2, Column: 42}, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 42, Line: 2, Column: 41}, + EndPos: ast.Position{Offset: 50, Line: 2, Column: 49}, + }, + }, + FunctionBlock: &ast.FunctionBlock{ + Block: &ast.Block{ + Statements: []ast.Statement{ + &ast.AssignmentStatement{ + Target: &ast.MemberExpression{ + Expression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "self", + Pos: ast.Position{Offset: 54, Line: 2, Column: 53}, + }, + }, + AccessPos: ast.Position{Offset: 58, Line: 2, Column: 57}, + Identifier: ast.Identifier{ + Identifier: "id", + Pos: ast.Position{Offset: 59, Line: 2, Column: 58}, + }, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationCopy, + Pos: ast.Position{Offset: 62, Line: 2, Column: 61}, + }, + Value: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "id", + Pos: ast.Position{Offset: 64, Line: 2, Column: 63}, + }, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 52, Line: 2, Column: 51}, + EndPos: ast.Position{Offset: 67, Line: 2, Column: 66}, + }, + }, + }, + StartPos: ast.Position{Offset: 38, Line: 2, Column: 37}, + }, + }, + }, + ), + Range: ast.Range{ + StartPos: ast.Position{Offset: 9, Line: 2, Column: 8}, + EndPos: ast.Position{Offset: 69, Line: 2, Column: 68}, + }, + }, + }, + result.Declarations(), + ) +} + +func TestParseAccessModifiers(t *testing.T) { + + t.Parallel() + + type declaration struct { + name, code string + } + + declarations := []declaration{ + {"variable", "%s var test = 1"}, + {"constant", "%s let test = 1"}, + {"function", "%s fun test() {}"}, + } + + for _, compositeKind := range common.AllCompositeKinds { + + for _, isInterface := range []bool{true, false} { + + if !compositeKind.SupportsInterfaces() && isInterface { + continue + } + + interfaceKeyword := "" + if isInterface { + interfaceKeyword = "interface" + } + + formatName := func(name string) string { + return fmt.Sprintf( + "%s %s %s", + compositeKind.Keyword(), + interfaceKeyword, + name, + ) + } + + var baseType = "" + + if compositeKind == common.CompositeKindAttachment { + baseType = "for AnyStruct" + } + + formatCode := func(format string) string { + return fmt.Sprintf(format, compositeKind.Keyword(), interfaceKeyword, baseType) + } + + if compositeKind == common.CompositeKindEvent { + declarations = append(declarations, + declaration{ + formatName("itself"), + formatCode("%%s %s %s Test()%s"), + }, + ) + } else { + declarations = append(declarations, + declaration{ + formatName("itself"), + formatCode("%%s %s %s Test %s {}"), + }, + declaration{ + formatName("field"), + formatCode("%s %s Test %s { %%s let test: Int ; init() { self.test = 1 } }"), + }, + declaration{ + formatName("function"), + formatCode("%s %s Test %s { %%s fun test() {} }"), + }, + ) + } + } + } + + for _, declaration := range declarations { + for _, access := range ast.BasicAccesses { + testName := fmt.Sprintf("%s/%s", declaration.name, access) + t.Run(testName, func(t *testing.T) { + program := fmt.Sprintf(declaration.code, access.Keyword()) + _, errs := testParseProgram(program) + + require.Empty(t, errs) + }) + } + } +} + +func TestParsePreconditionWithUnaryNegation(t *testing.T) { + + t.Parallel() + + const code = ` + fun test() { + pre { + true: "one" + !false: "two" + } + } + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.FunctionDeclaration{ + Access: ast.AccessNotSpecified, + Identifier: ast.Identifier{ + Identifier: "test", + Pos: ast.Position{Offset: 8, Line: 2, Column: 7}, + }, + ParameterList: &ast.ParameterList{ + Range: ast.Range{ + StartPos: ast.Position{Offset: 12, Line: 2, Column: 11}, + EndPos: ast.Position{Offset: 13, Line: 2, Column: 12}, + }, + }, + FunctionBlock: &ast.FunctionBlock{ + Block: &ast.Block{ + Range: ast.Range{ + StartPos: ast.Position{Offset: 15, Line: 2, Column: 14}, + EndPos: ast.Position{Offset: 105, Line: 7, Column: 6}, + }, + }, + PreConditions: &ast.Conditions{ + &ast.TestCondition{ + Test: &ast.BoolExpression{ + Value: true, + Range: ast.Range{ + StartPos: ast.Position{Offset: 47, Line: 4, Column: 14}, + EndPos: ast.Position{Offset: 50, Line: 4, Column: 17}, + }, + }, + Message: &ast.StringExpression{ + Value: "one", + Range: ast.Range{ + StartPos: ast.Position{Offset: 53, Line: 4, Column: 20}, + EndPos: ast.Position{Offset: 57, Line: 4, Column: 24}, + }, + }, + }, + &ast.TestCondition{ + Test: &ast.UnaryExpression{ + Operation: ast.OperationNegate, + Expression: &ast.BoolExpression{ + Value: false, + Range: ast.Range{ + StartPos: ast.Position{Offset: 74, Line: 5, Column: 15}, + EndPos: ast.Position{Offset: 78, Line: 5, Column: 19}, + }, + }, + StartPos: ast.Position{Offset: 73, Line: 5, Column: 14}, + }, + Message: &ast.StringExpression{ + Value: "two", + Range: ast.Range{ + StartPos: ast.Position{Offset: 81, Line: 5, Column: 22}, + EndPos: ast.Position{Offset: 85, Line: 5, Column: 26}, + }, + }, + }, + }, + }, + StartPos: ast.Position{Offset: 4, Line: 2, Column: 3}, + }, + }, + result.Declarations(), + ) +} + +func TestParseInvalidAccessModifiers(t *testing.T) { + + t.Parallel() + + t.Run("pragma", func(t *testing.T) { + + t.Parallel() + + _, errs := testParseDeclarations("pub #test") + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "invalid access modifier for pragma", + Pos: ast.Position{Offset: 0, Line: 1, Column: 0}, + }, + }, + errs, + ) + }) + + t.Run("transaction", func(t *testing.T) { + + t.Parallel() + + _, errs := testParseDeclarations("pub transaction {}") + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "invalid access modifier for transaction", + Pos: ast.Position{Offset: 0, Line: 1, Column: 0}, + }, + }, + errs, + ) + }) + + t.Run("transaction", func(t *testing.T) { + + t.Parallel() + + _, errs := testParseDeclarations("pub priv let x = 1") + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "invalid second access modifier", + Pos: ast.Position{Offset: 4, Line: 1, Column: 4}, + }, + }, + errs, + ) + }) +} + +func TestParseInvalidImportWithModifier(t *testing.T) { + + t.Parallel() + + t.Run("static, enabled", func(t *testing.T) { + + t.Parallel() + + _, errs := ParseDeclarations( + nil, + []byte(` + static import x from 0x1 + `), + Config{ + StaticModifierEnabled: true, + }, + ) + + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "invalid static modifier for import", + Pos: ast.Position{Offset: 17, Line: 2, Column: 16}, + }, + }, + errs, + ) + }) + + t.Run("static, disabled", func(t *testing.T) { + + t.Parallel() + + _, errs := testParseDeclarations(` + static import x from 0x1 + `) + + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "unexpected token: identifier", + Pos: ast.Position{Offset: 13, Line: 2, Column: 12}, + }, + }, + errs, + ) + }) + + t.Run("native, enabled", func(t *testing.T) { + + t.Parallel() + + _, errs := ParseDeclarations( + nil, + []byte(` + native import x from 0x1 + `), + Config{ + NativeModifierEnabled: true, + }, + ) + + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "invalid native modifier for import", + Pos: ast.Position{Offset: 17, Line: 2, Column: 16}, + }, + }, + errs, + ) + }) + + t.Run("native, disabled", func(t *testing.T) { + + t.Parallel() + + _, errs := testParseDeclarations(` + native import x from 0x1 + `) + + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "unexpected token: identifier", + Pos: ast.Position{Offset: 13, Line: 2, Column: 12}, + }, + }, + errs, + ) + }) +} + +func TestParseInvalidEventWithModifier(t *testing.T) { + + t.Parallel() + + t.Run("static, enabled", func(t *testing.T) { + + t.Parallel() + + _, errs := ParseDeclarations( + nil, + []byte(` + static event Foo() + `), + Config{ + StaticModifierEnabled: true, + }, + ) + + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "invalid static modifier for event", + Pos: ast.Position{Offset: 17, Line: 2, Column: 16}, + }, + }, + errs, + ) + }) + + t.Run("static, disabled", func(t *testing.T) { + + t.Parallel() + + _, errs := testParseDeclarations(` + static event Foo() + `) + + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "unexpected token: identifier", + Pos: ast.Position{Offset: 13, Line: 2, Column: 12}, + }, + }, + errs, + ) + }) + + t.Run("native, enabled", func(t *testing.T) { + + t.Parallel() + + _, errs := ParseDeclarations( + nil, + []byte(` + native event Foo() + `), + Config{ + NativeModifierEnabled: true, + }, + ) + + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "invalid native modifier for event", + Pos: ast.Position{Offset: 17, Line: 2, Column: 16}, + }, + }, + errs, + ) + }) + + t.Run("native, disabled", func(t *testing.T) { + + t.Parallel() + + _, errs := testParseDeclarations(` + native event Foo() + `) + + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "unexpected token: identifier", + Pos: ast.Position{Offset: 13, Line: 2, Column: 12}, + }, + }, + errs, + ) + }) + +} + +func TestParseCompositeWithModifier(t *testing.T) { + + t.Parallel() + + t.Run("static, enabled", func(t *testing.T) { + + t.Parallel() + + _, errs := ParseDeclarations( + nil, + []byte(` + static struct Foo() + `), + Config{ + StaticModifierEnabled: true, + }, + ) + + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "invalid static modifier for structure", + Pos: ast.Position{Offset: 17, Line: 2, Column: 16}, + }, + }, + errs, + ) + }) + + t.Run("static, disabled", func(t *testing.T) { + + t.Parallel() + + _, errs := testParseDeclarations(` + static struct Foo() + `) + + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "unexpected token: identifier", + Pos: ast.Position{Offset: 13, Line: 2, Column: 12}, + }, + }, + errs, + ) + }) + + t.Run("native, enabled", func(t *testing.T) { + + t.Parallel() + + _, errs := ParseDeclarations( + nil, + []byte(` + native struct Foo() + `), + Config{ + NativeModifierEnabled: true, + }, + ) + + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "invalid native modifier for structure", + Pos: ast.Position{Offset: 17, Line: 2, Column: 16}, + }, + }, + errs, + ) + }) + + t.Run("native, disabled", func(t *testing.T) { + + t.Parallel() + + _, errs := testParseDeclarations(` + native struct Foo() + `) + + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "unexpected token: identifier", + Pos: ast.Position{Offset: 13, Line: 2, Column: 12}, + }, + }, + errs, + ) + }) +} + +func TestParseTransactionWithModifier(t *testing.T) { + + t.Parallel() + + t.Run("static, enabled", func(t *testing.T) { + + t.Parallel() + + _, errs := ParseDeclarations( + nil, + []byte(` + static transaction {} + `), + Config{ + StaticModifierEnabled: true, + }, + ) + + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "invalid static modifier for transaction", + Pos: ast.Position{Offset: 17, Line: 2, Column: 16}, + }, + }, + errs, + ) + }) + + t.Run("static, disabled", func(t *testing.T) { + + t.Parallel() + + _, errs := testParseDeclarations(` + static transaction {} + `) + + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "unexpected token: identifier", + Pos: ast.Position{Offset: 13, Line: 2, Column: 12}, + }, + }, + errs, + ) + }) + + t.Run("native, enabled", func(t *testing.T) { + + t.Parallel() + + _, errs := ParseDeclarations( + nil, + []byte(` + native transaction {} + `), + Config{ + NativeModifierEnabled: true, + }, + ) + + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "invalid native modifier for transaction", + Pos: ast.Position{Offset: 17, Line: 2, Column: 16}, + }, + }, + errs, + ) + }) + + t.Run("native, disabled", func(t *testing.T) { + + t.Parallel() + + _, errs := testParseDeclarations(` + native transaction {} + `) + + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "unexpected token: identifier", + Pos: ast.Position{Offset: 13, Line: 2, Column: 12}, + }, + }, + errs, + ) + }) +} + +func TestParseNestedPragma(t *testing.T) { + + t.Parallel() + + parse := func(input string, config Config) (ast.Declaration, []error) { + return Parse( + nil, + []byte(input), + func(p *parser) (ast.Declaration, error) { + return parseMemberOrNestedDeclaration( + p, + "", + ) + }, + config, + ) + } + + t.Run("native, enabled", func(t *testing.T) { + + t.Parallel() + + _, errs := parse( + "native #foo", + Config{ + NativeModifierEnabled: true, + }, + ) + + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "invalid native modifier for pragma", + Pos: ast.Position{Offset: 0, Line: 1, Column: 0}, + }, + }, + errs, + ) + }) + + t.Run("native, disabled", func(t *testing.T) { + + t.Parallel() + + _, errs := parse("native #pragma", Config{}) + + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "unexpected token: identifier", + Pos: ast.Position{Offset: 0, Line: 1, Column: 0}, + }, + }, + errs, + ) + }) + + t.Run("static", func(t *testing.T) { + + t.Parallel() + + _, errs := parse( + "static #pragma", + Config{ + StaticModifierEnabled: true, + }, + ) + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "invalid static modifier for pragma", + Pos: ast.Position{Offset: 0, Line: 1, Column: 0}, + }, + }, + errs, + ) + }) + + t.Run("static, disabled", func(t *testing.T) { + + t.Parallel() + + _, errs := parse( + "static #pragma", + Config{}, + ) + + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "unexpected token: identifier", + Pos: ast.Position{Offset: 0, Line: 1, Column: 0}, + }, + }, + errs, + ) + }) + + t.Run("static native, enabled", func(t *testing.T) { + + t.Parallel() + + _, errs := parse( + "static native #pragma", + Config{ + StaticModifierEnabled: true, + NativeModifierEnabled: true, + }, + ) + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "invalid static modifier for pragma", + Pos: ast.Position{Offset: 0, Line: 1, Column: 0}, + }, + }, + errs, + ) + }) + + t.Run("static native, disabled", func(t *testing.T) { + + t.Parallel() + + _, errs := parse("static native #pragma", Config{}) + + // For now, leading unknown identifiers are valid. + // This will be rejected in Stable Cadence. + + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "unexpected identifier", + Pos: ast.Position{Offset: 7, Line: 1, Column: 7}, + }, + }, + errs, + ) + }) + + t.Run("native static, enabled", func(t *testing.T) { + + t.Parallel() + + _, errs := parse( + "native static #pragma", + Config{ + StaticModifierEnabled: true, + NativeModifierEnabled: true, + }, + ) + + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "invalid static modifier after native modifier", + Pos: ast.Position{Offset: 7, Line: 1, Column: 7}, + }, + }, + errs, + ) + }) + + t.Run("pub", func(t *testing.T) { + + t.Parallel() + + _, errs := parse("pub #pragma", Config{}) + + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "invalid access modifier for pragma", + Pos: ast.Position{Offset: 0, Line: 1, Column: 0}, + }, + }, + errs, + ) + }) + + t.Run("pub static native, enabled", func(t *testing.T) { + + t.Parallel() + + _, errs := parse( + "pub static native #pragma", + Config{ + StaticModifierEnabled: true, + NativeModifierEnabled: true, + }, + ) + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "invalid access modifier for pragma", + Pos: ast.Position{Offset: 0, Line: 1, Column: 0}, + }, + }, + errs, + ) + }) + + t.Run("pub static native, disabled", func(t *testing.T) { + + t.Parallel() + + _, errs := parse("pub static native #pragma", Config{}) + + // For now, leading unknown identifiers are valid. + // This will be rejected in Stable Cadence. + + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "unexpected identifier", + Pos: ast.Position{Offset: 11, Line: 1, Column: 11}, + }, + }, + errs, + ) + }) + +} + +func TestParseMemberDocStrings(t *testing.T) { + + t.Parallel() + + t.Run("functions", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseDeclarations(` + struct Test { + + /// noReturnNoBlock + fun noReturnNoBlock() + + /// returnNoBlock + fun returnNoBlock(): Int + + /// returnAndBlock + fun returnAndBlock(): String {} + } + `) + + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.CompositeDeclaration{ + Access: ast.AccessNotSpecified, + CompositeKind: common.CompositeKindStructure, + Identifier: ast.Identifier{ + Identifier: "Test", + Pos: ast.Position{Offset: 18, Line: 2, Column: 17}, + }, + Members: ast.NewUnmeteredMembers( + []ast.Declaration{ + &ast.FunctionDeclaration{ + Access: ast.AccessNotSpecified, + DocString: " noReturnNoBlock", + Identifier: ast.Identifier{ + Identifier: "noReturnNoBlock", + Pos: ast.Position{Offset: 78, Line: 5, Column: 18}, + }, + ParameterList: &ast.ParameterList{ + Range: ast.Range{ + StartPos: ast.Position{Offset: 93, Line: 5, Column: 33}, + EndPos: ast.Position{Offset: 94, Line: 5, Column: 34}, + }, + }, + StartPos: ast.Position{Offset: 74, Line: 5, Column: 14}, + }, + &ast.FunctionDeclaration{ + Access: ast.AccessNotSpecified, + DocString: " returnNoBlock", + Identifier: ast.Identifier{ + Identifier: "returnNoBlock", + Pos: ast.Position{Offset: 147, Line: 8, Column: 18}, + }, + ParameterList: &ast.ParameterList{ + Range: ast.Range{ + StartPos: ast.Position{Offset: 160, Line: 8, Column: 31}, + EndPos: ast.Position{Offset: 161, Line: 8, Column: 32}, + }, + }, + ReturnTypeAnnotation: &ast.TypeAnnotation{ + IsResource: false, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Int", + Pos: ast.Position{Offset: 164, Line: 8, Column: 35}, + }, + }, + StartPos: ast.Position{Offset: 164, Line: 8, Column: 35}, + }, + StartPos: ast.Position{Offset: 143, Line: 8, Column: 14}, + }, + &ast.FunctionDeclaration{ + Access: ast.AccessNotSpecified, + DocString: " returnAndBlock", + Identifier: ast.Identifier{ + Identifier: "returnAndBlock", + Pos: ast.Position{Offset: 220, Line: 11, Column: 18}, + }, + ParameterList: &ast.ParameterList{ + Range: ast.Range{ + StartPos: ast.Position{Offset: 234, Line: 11, Column: 32}, + EndPos: ast.Position{Offset: 235, Line: 11, Column: 33}, + }, + }, + ReturnTypeAnnotation: &ast.TypeAnnotation{ + IsResource: false, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "String", + Pos: ast.Position{Offset: 238, Line: 11, Column: 36}, + }, + }, + StartPos: ast.Position{Offset: 238, Line: 11, Column: 36}, + }, + FunctionBlock: &ast.FunctionBlock{ + Block: &ast.Block{ + Range: ast.Range{ + StartPos: ast.Position{Offset: 245, Line: 11, Column: 43}, + EndPos: ast.Position{Offset: 246, Line: 11, Column: 44}, + }, + }, + }, + StartPos: ast.Position{Offset: 216, Line: 11, Column: 14}, + }, + }, + ), + Range: ast.Range{ + StartPos: ast.Position{Offset: 11, Line: 2, Column: 10}, + EndPos: ast.Position{Offset: 258, Line: 12, Column: 10}, + }, + }, + }, + result, + ) + }) + + t.Run("special functions", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseDeclarations(` + struct Test { + + /// unknown + unknown() + + /// initNoBlock + init() + + /// destroyWithBlock + destroy() {} + } + `) + + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.CompositeDeclaration{ + Access: ast.AccessNotSpecified, + CompositeKind: common.CompositeKindStructure, + Identifier: ast.Identifier{ + Identifier: "Test", + Pos: ast.Position{Offset: 18, Line: 2, Column: 17}, + }, + Members: ast.NewUnmeteredMembers( + []ast.Declaration{ + &ast.SpecialFunctionDeclaration{ + Kind: common.DeclarationKindUnknown, + FunctionDeclaration: &ast.FunctionDeclaration{ + Access: ast.AccessNotSpecified, + DocString: " unknown", + Identifier: ast.Identifier{ + Identifier: "unknown", + Pos: ast.Position{Offset: 66, Line: 5, Column: 14}, + }, + ParameterList: &ast.ParameterList{ + Range: ast.Range{ + StartPos: ast.Position{Offset: 73, Line: 5, Column: 21}, + EndPos: ast.Position{Offset: 74, Line: 5, Column: 22}, + }, + }, + StartPos: ast.Position{Offset: 66, Line: 5, Column: 14}, + }, + }, + &ast.SpecialFunctionDeclaration{ + Kind: common.DeclarationKindInitializer, + FunctionDeclaration: &ast.FunctionDeclaration{ + Access: ast.AccessNotSpecified, + DocString: " initNoBlock", + Identifier: ast.Identifier{ + Identifier: "init", + Pos: ast.Position{Offset: 121, Line: 8, Column: 14}, + }, + ParameterList: &ast.ParameterList{ + Range: ast.Range{ + StartPos: ast.Position{Offset: 125, Line: 8, Column: 18}, + EndPos: ast.Position{Offset: 126, Line: 8, Column: 19}, + }, + }, + StartPos: ast.Position{Offset: 121, Line: 8, Column: 14}, + }, + }, + &ast.SpecialFunctionDeclaration{ + Kind: common.DeclarationKindDestructorLegacy, + FunctionDeclaration: &ast.FunctionDeclaration{ + Access: ast.AccessNotSpecified, + DocString: " destroyWithBlock", + Identifier: ast.Identifier{ + Identifier: "destroy", + Pos: ast.Position{Offset: 178, Line: 11, Column: 14}, + }, + ParameterList: &ast.ParameterList{ + Range: ast.Range{ + StartPos: ast.Position{Offset: 185, Line: 11, Column: 21}, + EndPos: ast.Position{Offset: 186, Line: 11, Column: 22}, + }, + }, + FunctionBlock: &ast.FunctionBlock{ + Block: &ast.Block{ + Range: ast.Range{ + StartPos: ast.Position{Offset: 188, Line: 11, Column: 24}, + EndPos: ast.Position{Offset: 189, Line: 11, Column: 25}, + }, + }, + }, + StartPos: ast.Position{Offset: 178, Line: 11, Column: 14}, + }, + }, + }, + ), + Range: ast.Range{ + StartPos: ast.Position{Offset: 11, Line: 2, Column: 10}, + EndPos: ast.Position{Offset: 201, Line: 12, Column: 10}, + }, + }, + }, + result, + ) + }) + +} + +func TestParseInvalidSpecialFunctionReturnTypeAnnotation(t *testing.T) { + + t.Parallel() + + _, errs := testParseDeclarations(` + struct Test { + + init(): Int + } + `) + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "invalid return type for initializer", + Pos: ast.Position{Offset: 40, Line: 4, Column: 18}, + }, + }, + errs, + ) +} diff --git a/runtime/old_parser/docstring.go b/runtime/old_parser/docstring.go new file mode 100644 index 0000000000..67559b286c --- /dev/null +++ b/runtime/old_parser/docstring.go @@ -0,0 +1,68 @@ +/* + * Cadence - The resource-oriented smart contract programming language + * + * Copyright Dapper Labs, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package old_parser + +import ( + "regexp" + "strings" +) + +var pragmaArgumentRegexp = regexp.MustCompile(`^\s+pragma\s+arguments\s+(.*)(?:\n|$)`) + +// ParseDocstringPragmaArguments parses the docstring and returns the values of all pragma arguments declarations. +// +// A pragma arguments declaration has the form `pragma arguments `, +// where is a Cadence argument list. +// +// The validity of the argument list is NOT checked by this function. +func ParseDocstringPragmaArguments(docString string) []string { + var pragmaArguments []string + + for _, line := range strings.Split(docString, "\n") { + match := pragmaArgumentRegexp.FindStringSubmatch(line) + if match == nil { + continue + } + pragmaArguments = append(pragmaArguments, match[1]) + } + + return pragmaArguments +} + +var pragmaSignersRegexp = regexp.MustCompile(`^\s+pragma\s+signers\s+(.*)(?:\n|$)`) + +// ParseDocstringPragmaSigners parses the docstring and returns the values of all pragma signers declarations. +// +// A pragma signers declaration has the form `pragma signers `, +// where is a list of strings. +// +// The validity of the argument list is NOT checked by this function. +func ParseDocstringPragmaSigners(docString string) []string { + var pragmaSigners []string + + for _, line := range strings.Split(docString, "\n") { + match := pragmaSignersRegexp.FindStringSubmatch(line) + if match == nil { + continue + } + pragmaSigners = append(pragmaSigners, match[1]) + } + + return pragmaSigners +} diff --git a/runtime/old_parser/docstring_test.go b/runtime/old_parser/docstring_test.go new file mode 100644 index 0000000000..5a7624cce3 --- /dev/null +++ b/runtime/old_parser/docstring_test.go @@ -0,0 +1,68 @@ +/* + * Cadence - The resource-oriented smart contract programming language + * + * Copyright Dapper Labs, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package old_parser + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestParseDocstringPragmaArguments(t *testing.T) { + + t.Parallel() + + actual := ParseDocstringPragmaArguments(` + pragma arguments (a: 1) + other stuff + + pragma arguments (b: 2) + + pragma arguments ( a: 1 , b: 2)`) + + require.Equal(t, + []string{ + "(a: 1)", + "(b: 2) ", + "( a: 1 , b: 2)", + }, + actual, + ) +} + +func TestParseDocstringPragmaSigners(t *testing.T) { + t.Parallel() + + actual := ParseDocstringPragmaSigners(` + pragma signers (alice) + other stuff + + pragma signers (alice, bob) + + pragma signers ( alice , bob )`) + + require.Equal(t, + []string{ + "(alice)", + "(alice, bob) ", + "( alice , bob )", + }, + actual, + ) +} diff --git a/runtime/old_parser/errors.go b/runtime/old_parser/errors.go new file mode 100644 index 0000000000..f081e2806c --- /dev/null +++ b/runtime/old_parser/errors.go @@ -0,0 +1,256 @@ +/* + * Cadence - The resource-oriented smart contract programming language + * + * Copyright Dapper Labs, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package old_parser + +import ( + "fmt" + "strings" + + "github.com/onflow/cadence/runtime/ast" + "github.com/onflow/cadence/runtime/common" + "github.com/onflow/cadence/runtime/errors" + "github.com/onflow/cadence/runtime/pretty" +) + +// Error + +type Error struct { + Code []byte + Errors []error +} + +func (e Error) Error() string { + var sb strings.Builder + sb.WriteString("Parsing failed:\n") + printErr := pretty.NewErrorPrettyPrinter(&sb, false). + PrettyPrintError(e, nil, map[common.Location][]byte{nil: e.Code}) + if printErr != nil { + panic(printErr) + } + return sb.String() +} + +func (e Error) ChildErrors() []error { + return e.Errors +} + +// ParserError + +type ParseError interface { + errors.UserError + ast.HasPosition + isParseError() +} + +// SyntaxError + +type SyntaxError struct { + Message string + Pos ast.Position +} + +func NewSyntaxError(pos ast.Position, message string, params ...any) *SyntaxError { + return &SyntaxError{ + Pos: pos, + Message: fmt.Sprintf(message, params...), + } +} + +func NewUnpositionedSyntaxError(message string, params ...any) *SyntaxError { + return &SyntaxError{ + Pos: ast.Position{Line: 1}, + Message: fmt.Sprintf(message, params...), + } +} + +var _ ParseError = &SyntaxError{} +var _ errors.UserError = &SyntaxError{} + +func (*SyntaxError) isParseError() {} + +func (*SyntaxError) IsUserError() {} + +func (e *SyntaxError) StartPosition() ast.Position { + return e.Pos +} + +func (e *SyntaxError) EndPosition(_ common.MemoryGauge) ast.Position { + return e.Pos +} + +func (e *SyntaxError) Error() string { + return e.Message +} + +// JuxtaposedUnaryOperatorsError + +type JuxtaposedUnaryOperatorsError struct { + Pos ast.Position +} + +var _ ParseError = &JuxtaposedUnaryOperatorsError{} +var _ errors.UserError = &JuxtaposedUnaryOperatorsError{} + +func (*JuxtaposedUnaryOperatorsError) isParseError() {} + +func (*JuxtaposedUnaryOperatorsError) IsUserError() {} + +func (e *JuxtaposedUnaryOperatorsError) StartPosition() ast.Position { + return e.Pos +} + +func (e *JuxtaposedUnaryOperatorsError) EndPosition(_ common.MemoryGauge) ast.Position { + return e.Pos +} + +func (e *JuxtaposedUnaryOperatorsError) Error() string { + return "unary operators must not be juxtaposed; parenthesize inner expression" +} + +// InvalidIntegerLiteralError + +type InvalidIntegerLiteralError struct { + Literal string + IntegerLiteralKind common.IntegerLiteralKind + InvalidIntegerLiteralKind InvalidNumberLiteralKind + ast.Range +} + +var _ ParseError = &InvalidIntegerLiteralError{} +var _ errors.UserError = &InvalidIntegerLiteralError{} +var _ errors.SecondaryError = &InvalidIntegerLiteralError{} + +func (*InvalidIntegerLiteralError) isParseError() {} + +func (*InvalidIntegerLiteralError) IsUserError() {} + +func (e *InvalidIntegerLiteralError) Error() string { + if e.IntegerLiteralKind == common.IntegerLiteralKindUnknown { + return fmt.Sprintf( + "invalid integer literal `%s`: %s", + e.Literal, + e.InvalidIntegerLiteralKind.Description(), + ) + } + + return fmt.Sprintf( + "invalid %s integer literal `%s`: %s", + e.IntegerLiteralKind.Name(), + e.Literal, + e.InvalidIntegerLiteralKind.Description(), + ) +} + +func (e *InvalidIntegerLiteralError) SecondaryError() string { + switch e.InvalidIntegerLiteralKind { + case InvalidNumberLiteralKindUnknown: + return "" + case InvalidNumberLiteralKindLeadingUnderscore: + return "remove the leading underscore" + case InvalidNumberLiteralKindTrailingUnderscore: + return "remove the trailing underscore" + case InvalidNumberLiteralKindUnknownPrefix: + return "did you mean `0x` (hexadecimal), `0b` (binary), or `0o` (octal)?" + case InvalidNumberLiteralKindMissingDigits: + return "consider adding a 0" + } + + panic(errors.NewUnreachableError()) +} + +// ExpressionDepthLimitReachedError is reported when the expression depth limit was reached +type ExpressionDepthLimitReachedError struct { + Pos ast.Position +} + +var _ ParseError = ExpressionDepthLimitReachedError{} +var _ errors.UserError = ExpressionDepthLimitReachedError{} + +func (ExpressionDepthLimitReachedError) isParseError() {} + +func (ExpressionDepthLimitReachedError) IsUserError() {} + +func (e ExpressionDepthLimitReachedError) Error() string { + return fmt.Sprintf( + "program too complex, reached max expression depth limit %d", + expressionDepthLimit, + ) +} + +func (e ExpressionDepthLimitReachedError) StartPosition() ast.Position { + return e.Pos +} + +func (e ExpressionDepthLimitReachedError) EndPosition(_ common.MemoryGauge) ast.Position { + return e.Pos +} + +// TypeDepthLimitReachedError is reported when the type depth limit was reached +// + +type TypeDepthLimitReachedError struct { + Pos ast.Position +} + +var _ ParseError = TypeDepthLimitReachedError{} +var _ errors.UserError = TypeDepthLimitReachedError{} + +func (TypeDepthLimitReachedError) isParseError() {} + +func (TypeDepthLimitReachedError) IsUserError() {} + +func (e TypeDepthLimitReachedError) Error() string { + return fmt.Sprintf( + "program too complex, reached max type depth limit %d", + typeDepthLimit, + ) +} + +func (e TypeDepthLimitReachedError) StartPosition() ast.Position { + return e.Pos +} + +func (e TypeDepthLimitReachedError) EndPosition(_ common.MemoryGauge) ast.Position { + return e.Pos +} + +// MissingCommaInParameterListError + +type MissingCommaInParameterListError struct { + Pos ast.Position +} + +var _ ParseError = &MissingCommaInParameterListError{} +var _ errors.UserError = &MissingCommaInParameterListError{} + +func (*MissingCommaInParameterListError) isParseError() {} + +func (*MissingCommaInParameterListError) IsUserError() {} + +func (e *MissingCommaInParameterListError) StartPosition() ast.Position { + return e.Pos +} + +func (e *MissingCommaInParameterListError) EndPosition(_ common.MemoryGauge) ast.Position { + return e.Pos +} + +func (e *MissingCommaInParameterListError) Error() string { + return "missing comma after parameter" +} diff --git a/runtime/old_parser/expression.go b/runtime/old_parser/expression.go new file mode 100644 index 0000000000..7ed3543351 --- /dev/null +++ b/runtime/old_parser/expression.go @@ -0,0 +1,1818 @@ +/* + * Cadence - The resource-oriented smart contract programming language + * + * Copyright Dapper Labs, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package old_parser + +import ( + "math/big" + "strings" + "unicode/utf8" + + "github.com/onflow/cadence/runtime/ast" + "github.com/onflow/cadence/runtime/common" + "github.com/onflow/cadence/runtime/errors" + "github.com/onflow/cadence/runtime/parser/lexer" +) + +const exprBindingPowerGap = 10 + +const ( + exprLeftBindingPowerTernary = exprBindingPowerGap * (iota + 2) + exprLeftBindingPowerLogicalOr + exprLeftBindingPowerLogicalAnd + exprLeftBindingPowerComparison + exprLeftBindingPowerNilCoalescing + exprLeftBindingPowerBitwiseOr + exprLeftBindingPowerBitwiseXor + exprLeftBindingPowerBitwiseAnd + exprLeftBindingPowerBitwiseShift + exprLeftBindingPowerAddition + exprLeftBindingPowerMultiplication + exprLeftBindingPowerCasting + exprLeftBindingPowerUnaryPrefix + exprLeftBindingPowerUnaryPostfix + exprLeftBindingPowerAccess +) + +type infixExprFunc func(parser *parser, left, right ast.Expression) (ast.Expression, error) +type prefixExprFunc func(parser *parser, right ast.Expression, tokenRange ast.Range) (ast.Expression, error) +type postfixExprFunc func(parser *parser, left ast.Expression, tokenRange ast.Range) (ast.Expression, error) +type exprNullDenotationFunc func(parser *parser, token lexer.Token) (ast.Expression, error) +type exprMetaLeftDenotationFunc func( + p *parser, + rightBindingPower int, + left ast.Expression, +) ( + result ast.Expression, + err error, + done bool, +) + +type literalExpr struct { + nullDenotation exprNullDenotationFunc + tokenType lexer.TokenType +} + +type infixExpr struct { + leftDenotation infixExprFunc + leftBindingPower int + tokenType lexer.TokenType + rightAssociative bool +} + +type binaryExpr struct { + leftBindingPower int + operation ast.Operation + tokenType lexer.TokenType + rightAssociative bool +} + +type prefixExpr struct { + nullDenotation prefixExprFunc + bindingPower int + tokenType lexer.TokenType +} + +type unaryExpr struct { + tokenType lexer.TokenType + bindingPower int + operation ast.Operation +} + +type postfixExpr struct { + leftDenotation postfixExprFunc + bindingPower int + tokenType lexer.TokenType +} + +var exprNullDenotations = [lexer.TokenMax]exprNullDenotationFunc{} + +type exprLeftDenotationFunc func(parser *parser, token lexer.Token, left ast.Expression) (ast.Expression, error) + +var exprLeftBindingPowers = [lexer.TokenMax]int{} +var exprIdentifierLeftBindingPowers = map[string]int{} +var exprLeftDenotations = [lexer.TokenMax]exprLeftDenotationFunc{} +var exprMetaLeftDenotations = [lexer.TokenMax]exprMetaLeftDenotationFunc{} + +func defineExpr(def any) { + switch def := def.(type) { + case infixExpr: + tokenType := def.tokenType + + setExprLeftBindingPower(tokenType, def.leftBindingPower) + + rightBindingPower := def.leftBindingPower + if def.rightAssociative { + rightBindingPower-- + } + + setExprLeftDenotation( + tokenType, + func(parser *parser, _ lexer.Token, left ast.Expression) (ast.Expression, error) { + right, err := parseExpression(parser, rightBindingPower) + if err != nil { + return nil, err + } + + return def.leftDenotation(parser, left, right) + }, + ) + + case binaryExpr: + defineExpr(infixExpr{ + tokenType: def.tokenType, + leftBindingPower: def.leftBindingPower, + rightAssociative: def.rightAssociative, + leftDenotation: func(p *parser, left, right ast.Expression) (ast.Expression, error) { + return ast.NewBinaryExpression( + p.memoryGauge, + def.operation, + left, + right, + ), nil + }, + }) + + case literalExpr: + tokenType := def.tokenType + setExprNullDenotation(tokenType, def.nullDenotation) + + case prefixExpr: + tokenType := def.tokenType + setExprNullDenotation( + tokenType, + func(parser *parser, token lexer.Token) (ast.Expression, error) { + right, err := parseExpression(parser, def.bindingPower) + if err != nil { + return nil, err + } + + return def.nullDenotation(parser, right, token.Range) + }, + ) + + case unaryExpr: + defineExpr(prefixExpr{ + tokenType: def.tokenType, + bindingPower: def.bindingPower, + nullDenotation: func(p *parser, right ast.Expression, tokenRange ast.Range) (ast.Expression, error) { + return ast.NewUnaryExpression( + p.memoryGauge, + def.operation, + right, + tokenRange.StartPos, + ), nil + }, + }) + + case postfixExpr: + tokenType := def.tokenType + setExprLeftBindingPower(tokenType, def.bindingPower) + setExprLeftDenotation( + tokenType, + func(p *parser, token lexer.Token, left ast.Expression) (ast.Expression, error) { + return def.leftDenotation(p, left, token.Range) + }, + ) + + default: + panic(errors.NewUnreachableError()) + } +} + +func setExprNullDenotation(tokenType lexer.TokenType, nullDenotation exprNullDenotationFunc) { + current := exprNullDenotations[tokenType] + if current != nil { + panic(errors.NewUnexpectedError( + "expression null denotation for token %s already exists", + tokenType, + )) + } + exprNullDenotations[tokenType] = nullDenotation +} + +func setExprLeftBindingPower(tokenType lexer.TokenType, power int) { + current := exprLeftBindingPowers[tokenType] + if current > power { + return + } + exprLeftBindingPowers[tokenType] = power +} + +func setExprIdentifierLeftBindingPower(keyword string, power int) { + current := exprIdentifierLeftBindingPowers[keyword] + if current > power { + return + } + exprIdentifierLeftBindingPowers[keyword] = power +} + +func setExprLeftDenotation(tokenType lexer.TokenType, leftDenotation exprLeftDenotationFunc) { + current := exprLeftDenotations[tokenType] + if current != nil { + panic(errors.NewUnexpectedError( + "expression left denotation for token %s already exists", + tokenType, + )) + } + + exprLeftDenotations[tokenType] = leftDenotation +} + +func setExprMetaLeftDenotation(tokenType lexer.TokenType, metaLeftDenotation exprMetaLeftDenotationFunc) { + current := exprMetaLeftDenotations[tokenType] + if current != nil { + panic(errors.NewUnexpectedError( + "expression meta left denotation for token %s already exists", + tokenType, + )) + } + exprMetaLeftDenotations[tokenType] = metaLeftDenotation +} + +// init defines the binding power for operations. +func init() { + + defineExpr(binaryExpr{ + tokenType: lexer.TokenVerticalBarVerticalBar, + leftBindingPower: exprLeftBindingPowerLogicalOr, + operation: ast.OperationOr, + }) + + defineExpr(binaryExpr{ + tokenType: lexer.TokenAmpersandAmpersand, + leftBindingPower: exprLeftBindingPowerLogicalAnd, + operation: ast.OperationAnd, + }) + + defineLessThanOrTypeArgumentsExpression() + defineGreaterThanOrBitwiseRightShiftExpression() + + defineExpr(binaryExpr{ + tokenType: lexer.TokenLessEqual, + leftBindingPower: exprLeftBindingPowerComparison, + operation: ast.OperationLessEqual, + }) + + defineExpr(binaryExpr{ + tokenType: lexer.TokenGreaterEqual, + leftBindingPower: exprLeftBindingPowerComparison, + operation: ast.OperationGreaterEqual, + }) + + defineExpr(binaryExpr{ + tokenType: lexer.TokenEqualEqual, + leftBindingPower: exprLeftBindingPowerComparison, + operation: ast.OperationEqual, + }) + + defineExpr(binaryExpr{ + tokenType: lexer.TokenNotEqual, + leftBindingPower: exprLeftBindingPowerComparison, + operation: ast.OperationNotEqual, + }) + + defineExpr(binaryExpr{ + tokenType: lexer.TokenDoubleQuestionMark, + leftBindingPower: exprLeftBindingPowerNilCoalescing, + operation: ast.OperationNilCoalesce, + rightAssociative: true, + }) + + defineExpr(binaryExpr{ + tokenType: lexer.TokenVerticalBar, + leftBindingPower: exprLeftBindingPowerBitwiseOr, + operation: ast.OperationBitwiseOr, + }) + + defineExpr(binaryExpr{ + tokenType: lexer.TokenCaret, + leftBindingPower: exprLeftBindingPowerBitwiseXor, + operation: ast.OperationBitwiseXor, + }) + + defineExpr(binaryExpr{ + tokenType: lexer.TokenAmpersand, + leftBindingPower: exprLeftBindingPowerBitwiseAnd, + operation: ast.OperationBitwiseAnd, + }) + + defineExpr(binaryExpr{ + tokenType: lexer.TokenLessLess, + leftBindingPower: exprLeftBindingPowerBitwiseShift, + operation: ast.OperationBitwiseLeftShift, + }) + + defineExpr(binaryExpr{ + tokenType: lexer.TokenPlus, + leftBindingPower: exprLeftBindingPowerAddition, + operation: ast.OperationPlus, + }) + + defineExpr(binaryExpr{ + tokenType: lexer.TokenMinus, + leftBindingPower: exprLeftBindingPowerAddition, + operation: ast.OperationMinus, + }) + + defineExpr(binaryExpr{ + tokenType: lexer.TokenStar, + leftBindingPower: exprLeftBindingPowerMultiplication, + operation: ast.OperationMul, + }) + + defineExpr(binaryExpr{ + tokenType: lexer.TokenSlash, + leftBindingPower: exprLeftBindingPowerMultiplication, + operation: ast.OperationDiv, + }) + + defineExpr(binaryExpr{ + tokenType: lexer.TokenPercent, + leftBindingPower: exprLeftBindingPowerMultiplication, + operation: ast.OperationMod, + }) + + defineIdentifierLeftDenotations() + + defineExpr(literalExpr{ + tokenType: lexer.TokenBinaryIntegerLiteral, + nullDenotation: func(p *parser, token lexer.Token) (ast.Expression, error) { + literal := p.tokenSource(token) + return parseIntegerLiteral( + p, + literal, + literal[2:], + common.IntegerLiteralKindBinary, + token.Range, + ), nil + }, + }) + + defineExpr(literalExpr{ + tokenType: lexer.TokenOctalIntegerLiteral, + nullDenotation: func(p *parser, token lexer.Token) (ast.Expression, error) { + literal := p.tokenSource(token) + return parseIntegerLiteral( + p, + literal, + literal[2:], + common.IntegerLiteralKindOctal, + token.Range, + ), nil + }, + }) + + defineExpr(literalExpr{ + tokenType: lexer.TokenDecimalIntegerLiteral, + nullDenotation: func(p *parser, token lexer.Token) (ast.Expression, error) { + literal := p.tokenSource(token) + return parseIntegerLiteral( + p, + literal, + literal, + common.IntegerLiteralKindDecimal, + token.Range, + ), nil + }, + }) + + defineExpr(literalExpr{ + tokenType: lexer.TokenHexadecimalIntegerLiteral, + nullDenotation: func(p *parser, token lexer.Token) (ast.Expression, error) { + literal := p.tokenSource(token) + return parseIntegerLiteral( + p, + literal, + literal[2:], + common.IntegerLiteralKindHexadecimal, + token.Range, + ), nil + }, + }) + + defineExpr(literalExpr{ + tokenType: lexer.TokenUnknownBaseIntegerLiteral, + nullDenotation: func(p *parser, token lexer.Token) (ast.Expression, error) { + literal := p.tokenSource(token) + return parseIntegerLiteral( + p, + literal, + literal[2:], + common.IntegerLiteralKindUnknown, + token.Range, + ), nil + }, + }) + + defineExpr(literalExpr{ + tokenType: lexer.TokenFixedPointNumberLiteral, + nullDenotation: func(p *parser, token lexer.Token) (ast.Expression, error) { + literal := p.tokenSource(token) + return parseFixedPointLiteral( + p, + literal, + token.Range, + ), nil + }, + }) + + defineExpr(literalExpr{ + tokenType: lexer.TokenString, + nullDenotation: func(p *parser, token lexer.Token) (ast.Expression, error) { + literal := p.tokenSource(token) + parsedString := parseStringLiteral(p, literal) + return ast.NewStringExpression( + p.memoryGauge, + parsedString, + token.Range, + ), nil + }, + }) + + defineExpr(prefixExpr{ + tokenType: lexer.TokenMinus, + bindingPower: exprLeftBindingPowerUnaryPrefix, + nullDenotation: func(p *parser, right ast.Expression, tokenRange ast.Range) (ast.Expression, error) { + switch right := right.(type) { + case *ast.IntegerExpression: + if right.Value.Sign() > 0 { + if right.Value != nil { + right.Value.Neg(right.Value) + } + right.StartPos = tokenRange.StartPos + return right, nil + } + + case *ast.FixedPointExpression: + if !right.Negative { + right.Negative = !right.Negative + right.StartPos = tokenRange.StartPos + return right, nil + } + } + + return ast.NewUnaryExpression( + p.memoryGauge, + ast.OperationMinus, + right, + tokenRange.StartPos, + ), nil + }, + }) + + defineExpr(unaryExpr{ + tokenType: lexer.TokenExclamationMark, + bindingPower: exprLeftBindingPowerUnaryPrefix, + operation: ast.OperationNegate, + }) + + defineExpr(unaryExpr{ + tokenType: lexer.TokenLeftArrow, + bindingPower: exprLeftBindingPowerUnaryPrefix, + operation: ast.OperationMove, + }) + + defineExpr(postfixExpr{ + tokenType: lexer.TokenExclamationMark, + bindingPower: exprLeftBindingPowerUnaryPostfix, + leftDenotation: func(p *parser, left ast.Expression, tokenRange ast.Range) (ast.Expression, error) { + return ast.NewForceExpression( + p.memoryGauge, + left, + tokenRange.EndPos, + ), nil + }, + }) + + defineNestedExpression() + defineInvocationExpression() + defineArrayExpression() + defineDictionaryExpression() + defineIndexExpression() + definePathExpression() + defineConditionalExpression() + defineReferenceExpression() + defineMemberExpression() + defineIdentifierExpression() + + setExprNullDenotation(lexer.TokenEOF, func(parser *parser, token lexer.Token) (ast.Expression, error) { + return nil, NewSyntaxError(token.StartPos, "unexpected end of program") + }) +} + +func defineLessThanOrTypeArgumentsExpression() { + + // The less token `<` does not have a single left binding power, + // but one depending on the tokens following it: + // + // Either an invocation with type arguments (zero or more, comma separated), + // followed by a closing greater token `>` and argument list; + // or a normal expression. + // + // lessThenOrTypeArguments : '<' + // ( ( ( typeAnnotation ( ',' )* )? '>' argumentList ) + // | expression + // ) + // + // + // Parse this ambiguity by first trying to parse type arguments + // and a closing greater token `>` and start of an argument list, + // i.e. the open paren token `(`. + // + // If that parse fails, the result expression must be a binary expression, + // and a normal expression must follow. + // + // In both cases, the right binding power must be checked, + // just like it is before a normal left denotation is applied. + + const binaryExpressionLeftBindingPower = exprLeftBindingPowerComparison + const invocationExpressionLeftBindingPower = exprLeftBindingPowerAccess + + setExprMetaLeftDenotation( + lexer.TokenLess, + func(p *parser, rightBindingPower int, left ast.Expression) (result ast.Expression, err error, done bool) { + + var typeArguments []*ast.TypeAnnotation + + // Start buffering before skipping the `<` token, + // so it can be replayed in case the right binding power + // was higher than the determined left binding power. + + p.startBuffering() + p.startAmbiguity() + defer p.endAmbiguity() + + // Skip the `<` token. + p.nextSemanticToken() + + // First, try to parse zero or more comma-separated type + // arguments (type annotations), a closing greater token `>`, + // and the start of an argument list, i.e. the open paren token `(`. + // + // This parse may fail, in which case we just ignore the error, + // except for fatal errors. + + var argumentsStartPos ast.Position + + err = func() error { + defer func() { + err := recover() + // MemoryError should abort parsing + _, ok := err.(errors.MemoryError) + if ok { + panic(err) + } + }() + + typeArguments, err = parseCommaSeparatedTypeAnnotations(p, lexer.TokenGreater) + if err != nil { + return err + } + + _, err = p.mustOne(lexer.TokenGreater) + if err != nil { + return err + } + + p.skipSpaceAndComments() + parenOpenToken, err := p.mustOne(lexer.TokenParenOpen) + if err != nil { + return err + } + + argumentsStartPos = parenOpenToken.EndPos + + return nil + }() + + // `err` is nil means the expression is an invocation + if err == nil { + + // The expression was determined to be an invocation. + // Still, it should have maybe not been parsed if the right binding power + // was higher. In that case, replay the buffered tokens and stop. + + if rightBindingPower >= invocationExpressionLeftBindingPower { + err = p.replayBuffered() + if err != nil { + return nil, err, true + } + + return left, nil, true + } + + // The previous attempt to parse an invocation succeeded, + // accept the buffered tokens. + + p.acceptBuffered() + + arguments, endPos, err := parseArgumentListRemainder(p) + if err != nil { + return nil, err, true + } + + invocationExpression := ast.NewInvocationExpression( + p.memoryGauge, + left, + typeArguments, + arguments, + argumentsStartPos, + endPos, + ) + + return invocationExpression, nil, false + + } else { + + // The previous attempt to parse an invocation failed, + // replay the buffered tokens. + + err = p.replayBuffered() + if err != nil { + return nil, err, true + } + + // The expression was determined to *not* be an invocation, + // so it must be a binary expression. + // + // Like for a normal left denotation, + // check if this left denotation applies. + + if rightBindingPower >= binaryExpressionLeftBindingPower { + return left, nil, true + } + + // Skip the `<` token. + // The token buffering started before this token, + // because it should have maybe not been parsed in the first place + // if the right binding power is higher. + + p.nextSemanticToken() + + right, err := parseExpression(p, binaryExpressionLeftBindingPower) + if err != nil { + return nil, err, true + } + + binaryExpression := ast.NewBinaryExpression( + p.memoryGauge, + ast.OperationLess, + left, + right, + ) + + return binaryExpression, nil, false + } + }) +} + +// defineGreaterThanOrBitwiseRightShiftExpression parses +// the greater-than expression (operator `>`, e.g. `1 > 2`) +// and the bitwise right shift expression (operator `>>`, e.g. `1 >> 3`). +// +// The `>>` operator consists of two `>` tokens, instead of one dedicated `>>` token, +// because that would introduce a parsing problem for function calls/invocations +// which have a type argument, where the type argument is a type instantiation, +// for example, `f>()`. +func defineGreaterThanOrBitwiseRightShiftExpression() { + + setExprMetaLeftDenotation( + lexer.TokenGreater, + func(p *parser, rightBindingPower int, left ast.Expression) (result ast.Expression, err error, done bool) { + + // If the right binding power is higher than any of the potential cases, + // then return early + + if rightBindingPower >= exprLeftBindingPowerBitwiseShift && + rightBindingPower >= exprLeftBindingPowerComparison { + + return left, nil, true + } + + // Perform a lookahead for '>' + + current := p.current + cursor := p.tokens.Cursor() + + // Skip the `>` token. + p.next() + + // If another '>' token appears immediately, + // then the operator is actually a bitwise right shift operator + + isBitwiseShift := p.current.Is(lexer.TokenGreater) + + var operation ast.Operation + var nextRightBindingPower int + + if isBitwiseShift { + + operation = ast.OperationBitwiseRightShift + + // The expression was determined to be a bitwise shift. + // Still, it should have maybe not been parsed if the right binding power + // was higher. In that case, replay the buffered tokens and stop. + + if rightBindingPower >= exprLeftBindingPowerBitwiseShift { + p.current = current + p.tokens.Revert(cursor) + + return left, nil, true + } + + // The previous attempt to parse a bitwise right shift succeeded, + // accept the buffered tokens. + + nextRightBindingPower = exprLeftBindingPowerBitwiseShift + + } else { + + operation = ast.OperationGreater + + // The previous attempt to parse a bitwise right shift failed, + // replay the buffered tokens. + + p.current = current + p.tokens.Revert(cursor) + + // The expression was determined to *not* be a bitwise shift, + // so it must be a comparison expression. + // + // Like for a normal left denotation, + // check if this left denotation applies. + + if rightBindingPower >= exprLeftBindingPowerComparison { + return left, nil, true + } + + nextRightBindingPower = exprLeftBindingPowerComparison + } + + p.nextSemanticToken() + + right, err := parseExpression(p, nextRightBindingPower) + if err != nil { + return nil, err, true + } + + binaryExpression := ast.NewBinaryExpression( + p.memoryGauge, + operation, + left, + right, + ) + + return binaryExpression, err, false + }) +} + +func defineIdentifierExpression() { + defineExpr(literalExpr{ + tokenType: lexer.TokenIdentifier, + nullDenotation: func(p *parser, token lexer.Token) (ast.Expression, error) { + switch string(p.tokenSource(token)) { + case keywordTrue: + return ast.NewBoolExpression(p.memoryGauge, true, token.Range), nil + + case keywordFalse: + return ast.NewBoolExpression(p.memoryGauge, false, token.Range), nil + + case keywordNil: + return ast.NewNilExpression(p.memoryGauge, token.Range.StartPos), nil + + case keywordCreate: + return parseCreateExpressionRemainder(p, token) + + case keywordDestroy: + expression, err := parseExpression(p, lowestBindingPower) + if err != nil { + return nil, err + } + + return ast.NewDestroyExpression( + p.memoryGauge, + expression, + token.Range.StartPos, + ), nil + + case keywordAttach: + return parseAttachExpressionRemainder(p, token) + + case keywordFun: + return parseFunctionExpression(p, token) + + default: + return ast.NewIdentifierExpression( + p.memoryGauge, + p.tokenToIdentifier(token), + ), nil + } + }, + }) +} + +func parseFunctionExpression(p *parser, token lexer.Token) (*ast.FunctionExpression, error) { + + parameterList, returnTypeAnnotation, functionBlock, err := + parseFunctionParameterListAndRest(p, false) + if err != nil { + return nil, err + } + + return ast.NewFunctionExpression( + p.memoryGauge, + ast.FunctionPurityUnspecified, + parameterList, + returnTypeAnnotation, + functionBlock, + token.StartPos, + ), nil +} + +func defineIdentifierLeftDenotations() { + + setExprIdentifierLeftBindingPower(keywordAs, exprLeftBindingPowerCasting) + setExprLeftDenotation( + lexer.TokenIdentifier, + func(parser *parser, t lexer.Token, left ast.Expression) (ast.Expression, error) { + // NOTE: switch statement with just one case instead of if, + // as this function is called for *any identifier left denotation ("postfix keyword"), + // not just for `as`, it might be extended with more cases (keywords) in the future + switch string(parser.tokenSource(t)) { + case keywordAs: + right, err := parseTypeAnnotation(parser) + if err != nil { + return nil, err + } + + return ast.NewCastingExpression( + parser.memoryGauge, + left, + ast.OperationCast, + right, + nil, + ), nil + + default: + panic(errors.NewUnreachableError()) + } + }, + ) + + for _, tokenOperation := range []struct { + token lexer.TokenType + operation ast.Operation + }{ + { + token: lexer.TokenAsExclamationMark, + operation: ast.OperationForceCast, + }, + { + token: lexer.TokenAsQuestionMark, + operation: ast.OperationFailableCast, + }, + } { + operation := tokenOperation.operation + tokenType := tokenOperation.token + + // Rebind operation, so the closure captures to current iteration's value, + // i.e. the next iteration doesn't override `operation` + + leftDenotation := (func(operation ast.Operation) exprLeftDenotationFunc { + return func(parser *parser, t lexer.Token, left ast.Expression) (ast.Expression, error) { + right, err := parseTypeAnnotation(parser) + if err != nil { + return nil, err + } + + return ast.NewCastingExpression( + parser.memoryGauge, + left, + operation, + right, + nil, + ), nil + } + })(operation) + + setExprLeftBindingPower(tokenType, exprLeftBindingPowerCasting) + setExprLeftDenotation(tokenType, leftDenotation) + } +} + +func parseCreateExpressionRemainder(p *parser, token lexer.Token) (*ast.CreateExpression, error) { + invocation, err := parseNominalTypeInvocationRemainder(p) + if err != nil { + return nil, err + } + + return ast.NewCreateExpression( + p.memoryGauge, + invocation, + token.StartPos, + ), nil +} + +func parseAttachExpressionRemainder(p *parser, token lexer.Token) (*ast.AttachExpression, error) { + attachment, err := parseNominalTypeInvocationRemainder(p) + + if err != nil { + return nil, err + } + + p.skipSpaceAndComments() + + if !p.isToken(p.current, lexer.TokenIdentifier, keywordTo) { + return nil, p.syntaxError( + "expected 'to', got %s", + p.current.Type, + ) + } + + // consume the `to` token + p.nextSemanticToken() + + base, err := parseExpression(p, lowestBindingPower) + + if err != nil { + return nil, err + } + + return ast.NewAttachExpression(p.memoryGauge, base, attachment, token.StartPos), nil +} + +// Invocation Expression Grammar: +// +// invocation : '(' ( argument ( ',' argument )* )? ')' +func defineInvocationExpression() { + setExprLeftBindingPower(lexer.TokenParenOpen, exprLeftBindingPowerAccess) + + setExprLeftDenotation( + lexer.TokenParenOpen, + func(p *parser, token lexer.Token, left ast.Expression) (ast.Expression, error) { + arguments, endPos, err := parseArgumentListRemainder(p) + if err != nil { + return nil, err + } + + return ast.NewInvocationExpression( + p.memoryGauge, + left, + nil, + arguments, + token.EndPos, + endPos, + ), nil + }, + ) +} + +func parseArgumentListRemainder(p *parser) (arguments []*ast.Argument, endPos ast.Position, err error) { + atEnd := false + expectArgument := true + for !atEnd { + p.skipSpaceAndComments() + + switch p.current.Type { + case lexer.TokenComma: + if expectArgument { + return nil, ast.EmptyPosition, p.syntaxError( + "expected argument or end of argument list, got %s", + p.current.Type, + ) + } + // Skip the comma + p.next() + expectArgument = true + + case lexer.TokenParenClose: + endPos = p.current.EndPos + // Skip the closing paren + p.next() + atEnd = true + + case lexer.TokenEOF: + return nil, + ast.EmptyPosition, + p.syntaxError("missing ')' at end of invocation argument list") + + default: + if !expectArgument { + return nil, + ast.EmptyPosition, + p.syntaxError( + "unexpected argument in argument list (expecting delimiter or end of argument list), got %s", + p.current.Type, + ) + } + + argument, err := parseArgument(p) + if err != nil { + return nil, ast.EmptyPosition, err + } + + p.skipSpaceAndComments() + + argument.TrailingSeparatorPos = p.current.StartPos + + arguments = append(arguments, argument) + + expectArgument = false + } + } + return +} + +// parseArgument parses an argument in an invocation. +// +// argument : (identifier ':' )? expression +func parseArgument(p *parser) (*ast.Argument, error) { + var label string + var labelStartPos, labelEndPos ast.Position + + expr, err := parseExpression(p, lowestBindingPower) + if err != nil { + return nil, err + } + + p.skipSpaceAndComments() + + // If a colon follows the expression, the expression was our label. + if p.current.Is(lexer.TokenColon) { + labelEndPos = p.current.EndPos + + identifier, ok := expr.(*ast.IdentifierExpression) + if !ok { + return nil, p.syntaxError( + "expected identifier for label, got %s", + expr, + ) + } + label = identifier.Identifier.Identifier + labelStartPos = expr.StartPosition() + + // Skip the identifier + p.nextSemanticToken() + + expr, err = parseExpression(p, lowestBindingPower) + if err != nil { + return nil, err + } + } + + if len(label) > 0 { + return ast.NewArgument( + p.memoryGauge, + label, + &labelStartPos, + &labelEndPos, + expr, + ), nil + } + return ast.NewUnlabeledArgument(p.memoryGauge, expr), nil +} + +func defineNestedExpression() { + setExprNullDenotation( + lexer.TokenParenOpen, + func(p *parser, startToken lexer.Token) (ast.Expression, error) { + p.skipSpaceAndComments() + + // special case: parse a Void literal `()` + if p.current.Type == lexer.TokenParenClose { + // skip the closing parenthesis + p.next() + + voidExpr := ast.NewVoidExpression(p.memoryGauge, startToken.StartPos, p.current.EndPos) + return voidExpr, nil + } + + expression, err := parseExpression(p, lowestBindingPower) + if err != nil { + return nil, err + } + + _, err = p.mustOne(lexer.TokenParenClose) + return expression, err + }, + ) +} + +func defineArrayExpression() { + setExprNullDenotation( + lexer.TokenBracketOpen, + func(p *parser, startToken lexer.Token) (ast.Expression, error) { + p.skipSpaceAndComments() + + var values []ast.Expression + for !p.current.Is(lexer.TokenBracketClose) { + p.skipSpaceAndComments() + if len(values) > 0 { + if !p.current.Is(lexer.TokenComma) { + break + } + p.next() + } + + value, err := parseExpression(p, lowestBindingPower) + if err != nil { + return nil, err + } + values = append(values, value) + } + + endToken, err := p.mustOne(lexer.TokenBracketClose) + if err != nil { + return nil, err + } + + return ast.NewArrayExpression( + p.memoryGauge, + values, + ast.NewRange( + p.memoryGauge, + startToken.StartPos, + endToken.EndPos, + ), + ), nil + }, + ) +} + +func defineDictionaryExpression() { + setExprNullDenotation( + lexer.TokenBraceOpen, + func(p *parser, startToken lexer.Token) (ast.Expression, error) { + p.skipSpaceAndComments() + + var entries []ast.DictionaryEntry + for !p.current.Is(lexer.TokenBraceClose) { + p.skipSpaceAndComments() + if len(entries) > 0 { + if !p.current.Is(lexer.TokenComma) { + break + } + p.next() + } + + key, err := parseExpression(p, lowestBindingPower) + if err != nil { + return nil, err + } + + _, err = p.mustOne(lexer.TokenColon) + if err != nil { + return nil, err + } + + value, err := parseExpression(p, lowestBindingPower) + if err != nil { + return nil, err + } + + entries = append(entries, ast.NewDictionaryEntry( + p.memoryGauge, + key, + value, + )) + } + + endToken, err := p.mustOne(lexer.TokenBraceClose) + if err != nil { + return nil, err + } + + return ast.NewDictionaryExpression( + p.memoryGauge, + entries, + ast.NewRange( + p.memoryGauge, + startToken.StartPos, + endToken.EndPos, + ), + ), nil + }, + ) +} + +func defineIndexExpression() { + setExprLeftBindingPower(lexer.TokenBracketOpen, exprLeftBindingPowerAccess) + setExprLeftDenotation( + lexer.TokenBracketOpen, + func(p *parser, token lexer.Token, left ast.Expression) (ast.Expression, error) { + firstIndexExpr, err := parseExpression(p, lowestBindingPower) + if err != nil { + return nil, err + } + + endToken, err := p.mustOne(lexer.TokenBracketClose) + if err != nil { + return nil, err + } + + return ast.NewIndexExpression( + p.memoryGauge, + left, + firstIndexExpr, + ast.NewRange( + p.memoryGauge, + token.StartPos, + endToken.EndPos, + ), + ), nil + }, + ) +} + +func defineConditionalExpression() { + setExprLeftBindingPower(lexer.TokenQuestionMark, exprLeftBindingPowerTernary) + setExprLeftDenotation( + lexer.TokenQuestionMark, + func(p *parser, _ lexer.Token, left ast.Expression) (ast.Expression, error) { + testExpression := left + thenExpression, err := parseExpression(p, lowestBindingPower) + if err != nil { + return nil, err + } + + _, err = p.mustOne(lexer.TokenColon) + if err != nil { + return nil, err + } + + elseExpression, err := parseExpression(p, lowestBindingPower) + if err != nil { + return nil, err + } + + return ast.NewConditionalExpression( + p.memoryGauge, + testExpression, + thenExpression, + elseExpression, + ), nil + }, + ) +} + +func definePathExpression() { + setExprNullDenotation( + lexer.TokenSlash, + func(p *parser, token lexer.Token) (ast.Expression, error) { + domain, err := p.mustIdentifier() + if err != nil { + return nil, err + } + + _, err = p.mustOne(lexer.TokenSlash) + if err != nil { + return nil, err + } + + identifier, err := p.mustIdentifier() + if err != nil { + return nil, err + } + + return ast.NewPathExpression( + p.memoryGauge, + domain, + identifier, + token.StartPos, + ), nil + }, + ) +} + +func defineReferenceExpression() { + setExprNullDenotation( + lexer.TokenAmpersand, + func(p *parser, token lexer.Token) (ast.Expression, error) { + p.skipSpaceAndComments() + expression, err := parseExpression(p, exprLeftBindingPowerCasting-exprBindingPowerGap) + if err != nil { + return nil, err + } + + castingExpression, ok := expression.(*ast.CastingExpression) + if !ok { + return nil, p.syntaxError("expected casting expression") + } + + p.skipSpaceAndComments() + + return ast.NewReferenceExpression( + p.memoryGauge, + castingExpression.Expression, + token.StartPos, + ), nil + }, + ) +} + +func defineMemberExpression() { + + setExprLeftBindingPower(lexer.TokenDot, exprLeftBindingPowerAccess) + setExprLeftDenotation( + lexer.TokenDot, + func(p *parser, token lexer.Token, left ast.Expression) (ast.Expression, error) { + return parseMemberAccess(p, token, left, false), nil + }, + ) + + setExprLeftBindingPower(lexer.TokenQuestionMarkDot, exprLeftBindingPowerAccess) + setExprLeftDenotation( + lexer.TokenQuestionMarkDot, + func(p *parser, token lexer.Token, left ast.Expression) (ast.Expression, error) { + return parseMemberAccess(p, token, left, true), nil + }, + ) +} + +func parseMemberAccess(p *parser, token lexer.Token, left ast.Expression, optional bool) ast.Expression { + + // Whitespace after '.' (dot token) and '?.' (question mark dot token) is not allowed. + // We parse it anyway and report an error + + if p.current.Is(lexer.TokenSpace) { + errorPos := p.current.StartPos + p.skipSpaceAndComments() + p.report(NewSyntaxError( + errorPos, + "invalid whitespace after %s", + lexer.TokenDot, + )) + } + + // If there is an identifier, use it. + // If not, report an error + + var identifier ast.Identifier + if p.current.Is(lexer.TokenIdentifier) { + identifier = p.tokenToIdentifier(p.current) + p.next() + } else { + p.reportSyntaxError( + "expected member name, got %s", + p.current.Type, + ) + } + + return ast.NewMemberExpression( + p.memoryGauge, + left, + optional, + // NOTE: use the end position, because the token + // can be an optional access token `?.` + token.EndPos, + identifier, + ) +} + +func exprLeftDenotationAllowsNewlineAfterNullDenotation(tokenType lexer.TokenType) bool { + + // Some tokens do not support newlines before them, + // as this could lead to ambiguities and potential underhanded code + + switch tokenType { + case + // '!': postfix force-unwrap VS unary prefix negation + lexer.TokenExclamationMark, + // '(': invocation VS parenthesized expression + lexer.TokenParenOpen, + // '[': indexing VS array literal + lexer.TokenBracketOpen: + + return false + } + + return true +} + +// parseExpression uses "Top-Down operator precedence parsing" (TDOP) technique to +// parse expressions. +func parseExpression(p *parser, rightBindingPower int) (ast.Expression, error) { + + if p.expressionDepth == expressionDepthLimit { + return nil, ExpressionDepthLimitReachedError{ + Pos: p.current.StartPos, + } + } + + p.expressionDepth++ + defer func() { + p.expressionDepth-- + }() + + p.skipSpaceAndComments() + t := p.current + p.next() + + left, err := applyExprNullDenotation(p, t) + if err != nil { + return nil, err + } + + for { + // Automatically skip any trivia between the left and right expression. + // However, do not automatically skip newlines: + // Some left denotations do not support newlines before them, + // to avoid ambiguities and potential underhanded code + + p.parseTrivia(triviaOptions{ + skipNewlines: false, + }) + + current := p.current + if current.Is(lexer.TokenSpace) { + cursor := p.tokens.Cursor() + p.next() + if !exprLeftDenotationAllowsNewlineAfterNullDenotation(p.current.Type) { + p.tokens.Revert(cursor) + p.current = current + } + } + + var done bool + left, err, done = applyExprMetaLeftDenotation(p, rightBindingPower, left) + if err != nil { + return nil, err + } + + if done { + break + } + } + + return left, nil +} + +func applyExprMetaLeftDenotation( + p *parser, + rightBindingPower int, + left ast.Expression, +) ( + result ast.Expression, + err error, + done bool, +) { + // By default, left denotations are applied if the right binding power + // is less than the left binding power of the current token. + // + // Token-specific meta-left denotations allow customizing this, + // e.g. determining the left binding power based on parsing more tokens + // or performing look-ahead + + metaLeftDenotation := exprMetaLeftDenotations[p.current.Type] + if metaLeftDenotation == nil { + metaLeftDenotation = defaultExprMetaLeftDenotation + } + + return metaLeftDenotation(p, rightBindingPower, left) +} + +// defaultExprMetaLeftDenotation is the default expression left denotation, which applies +// if the right binding power is less than the left binding power of the current token +func defaultExprMetaLeftDenotation( + p *parser, + rightBindingPower int, + left ast.Expression, +) ( + result ast.Expression, + err error, + done bool, +) { + leftBindingPower, err := exprLeftBindingPower(p) + if err != nil { + return nil, err, true + } + + if rightBindingPower >= leftBindingPower { + return left, nil, true + } + + t := p.current + + p.next() + + result, err = applyExprLeftDenotation(p, t, left) + return result, err, false +} + +func exprLeftBindingPower(p *parser) (int, error) { + token := p.current + tokenType := token.Type + if tokenType == lexer.TokenIdentifier { + identifier := p.tokenSource(token) + return exprIdentifierLeftBindingPowers[string(identifier)], nil + } + return exprLeftBindingPowers[tokenType], nil +} + +func applyExprNullDenotation(p *parser, token lexer.Token) (ast.Expression, error) { + tokenType := token.Type + nullDenotation := exprNullDenotations[tokenType] + if nullDenotation == nil { + return nil, p.syntaxError("unexpected token in expression: %s", tokenType) + } + return nullDenotation(p, token) +} + +func applyExprLeftDenotation(p *parser, token lexer.Token, left ast.Expression) (ast.Expression, error) { + leftDenotation := exprLeftDenotations[token.Type] + if leftDenotation == nil { + return nil, p.syntaxError("unexpected token in expression: %s", token.Type) + } + return leftDenotation(p, token, left) +} + +// parseStringLiteral parses a whole string literal, including start and end quotes +func parseStringLiteral(p *parser, literal []byte) (result string) { + length := len(literal) + if length == 0 { + p.reportSyntaxError("missing start of string literal: expected '\"'") + return + } + + if length >= 1 { + first := literal[0] + if first != '"' { + p.reportSyntaxError("invalid start of string literal: expected '\"', got %q", first) + } + } + + missingEnd := false + endOffset := length + if length >= 2 { + lastIndex := length - 1 + last := literal[lastIndex] + if last != '"' { + missingEnd = true + } else { + endOffset = lastIndex + } + } else { + missingEnd = true + } + + result = parseStringLiteralContent(p, literal[1:endOffset]) + + if missingEnd { + p.reportSyntaxError("invalid end of string literal: missing '\"'") + } + + return +} + +// parseStringLiteralContent parses the string literalExpr contents, excluding start and end quotes +func parseStringLiteralContent(p *parser, s []byte) (result string) { + + var builder strings.Builder + defer func() { + result = builder.String() + }() + + length := len(s) + + var r rune + index := 0 + + atEnd := index >= length + + advance := func() { + if atEnd { + r = lexer.EOF + return + } + + var width int + r, width = utf8.DecodeRune(s[index:]) + index += width + + atEnd = index >= length + } + + for index < length { + advance() + + if r != '\\' { + builder.WriteRune(r) + continue + } + + if atEnd { + p.reportSyntaxError("incomplete escape sequence: missing character after escape character") + return + } + + advance() + + switch r { + case '0': + builder.WriteByte(0) + case 'n': + builder.WriteByte('\n') + case 'r': + builder.WriteByte('\r') + case 't': + builder.WriteByte('\t') + case '"': + builder.WriteByte('"') + case '\'': + builder.WriteByte('\'') + case '\\': + builder.WriteByte('\\') + case 'u': + if atEnd { + p.reportSyntaxError( + "incomplete Unicode escape sequence: missing character '{' after escape character", + ) + return + } + advance() + if r != '{' { + p.reportSyntaxError("invalid Unicode escape sequence: expected '{', got %q", r) + continue + } + + var r2 rune + valid := true + digitIndex := 0 + for ; !atEnd && digitIndex < 8; digitIndex++ { + advance() + if r == '}' { + break + } + + parsed := parseHex(r) + + if parsed < 0 { + p.reportSyntaxError("invalid Unicode escape sequence: expected hex digit, got %q", r) + valid = false + } else { + r2 = r2<<4 | parsed + } + } + + if digitIndex > 0 && valid { + builder.WriteRune(r2) + } + + if r != '}' { + advance() + } + + switch r { + case '}': + break + case lexer.EOF: + p.reportSyntaxError( + "incomplete Unicode escape sequence: missing character '}' after escape character", + ) + default: + p.reportSyntaxError("incomplete Unicode escape sequence: expected '}', got %q", r) + } + + default: + // TODO: include index/column in error + p.reportSyntaxError("invalid escape character: %q", r) + // skip invalid escape character, don't write to result + } + } + + return +} + +func parseHex(r rune) rune { + switch { + case '0' <= r && r <= '9': + return r - '0' + case 'a' <= r && r <= 'f': + return r - 'a' + 10 + case 'A' <= r && r <= 'F': + return r - 'A' + 10 + } + + return -1 +} + +func parseIntegerLiteral(p *parser, literal, text []byte, kind common.IntegerLiteralKind, tokenRange ast.Range) *ast.IntegerExpression { + + report := func(invalidKind InvalidNumberLiteralKind) { + p.report( + &InvalidIntegerLiteralError{ + IntegerLiteralKind: kind, + InvalidIntegerLiteralKind: invalidKind, + // NOTE: not using text, because it has the base-prefix stripped + Literal: string(literal), + Range: tokenRange, + }, + ) + } + + // TODO: improve + s := string(text) + + // check literal has no leading underscore + if strings.HasPrefix(s, "_") { + report(InvalidNumberLiteralKindLeadingUnderscore) + } + + // check literal has no trailing underscore + if strings.HasSuffix(s, "_") { + report(InvalidNumberLiteralKindTrailingUnderscore) + } + + withoutUnderscores := strings.ReplaceAll(s, "_", "") + + var value *big.Int + var base int + + if kind == common.IntegerLiteralKindUnknown { + base = 1 + + report(InvalidNumberLiteralKindUnknownPrefix) + } else { + base = kind.Base() + + if withoutUnderscores == "" { + report(InvalidNumberLiteralKindMissingDigits) + } else { + estimatedSize := common.OverEstimateBigIntFromString(withoutUnderscores, kind) + common.UseMemory(p.memoryGauge, common.NewBigIntMemoryUsage(estimatedSize)) + + var ok bool + value, ok = new(big.Int).SetString(withoutUnderscores, base) + if !ok { + report(InvalidNumberLiteralKindUnknown) + } + } + } + + if value == nil { + common.UseMemory(p.memoryGauge, common.NewBigIntMemoryUsage(1)) + + value = new(big.Int) + } + + return ast.NewIntegerExpression(p.memoryGauge, literal, value, base, tokenRange) +} + +func parseFixedPointPart(gauge common.MemoryGauge, part string) (integer *big.Int, scale uint) { + withoutUnderscores := strings.ReplaceAll(part, "_", "") + + base := common.IntegerLiteralKindDecimal + + common.UseMemory( + gauge, + common.NewBigIntMemoryUsage( + common.OverEstimateBigIntFromString(withoutUnderscores, base), + ), + ) + + estimatedSize := common.OverEstimateBigIntFromString(withoutUnderscores, base) + common.UseMemory(gauge, common.NewBigIntMemoryUsage(estimatedSize)) + + integer, _ = new(big.Int).SetString(withoutUnderscores, base.Base()) + if integer == nil { + integer = new(big.Int) + } + scale = uint(len(withoutUnderscores)) + if scale == 0 { + scale = 1 + } + return integer, scale +} + +func parseFixedPointLiteral(p *parser, literal []byte, tokenRange ast.Range) *ast.FixedPointExpression { + // TODO: improve + parts := strings.Split(string(literal), ".") + integer, _ := parseFixedPointPart(p.memoryGauge, parts[0]) + fractional, scale := parseFixedPointPart(p.memoryGauge, parts[1]) + + return ast.NewFixedPointExpression( + p.memoryGauge, + literal, + false, + integer, + fractional, + scale, + tokenRange, + ) +} diff --git a/runtime/old_parser/expression_test.go b/runtime/old_parser/expression_test.go new file mode 100644 index 0000000000..79fa2c9b48 --- /dev/null +++ b/runtime/old_parser/expression_test.go @@ -0,0 +1,6458 @@ +/* + * Cadence - The resource-oriented smart contract programming language + * + * Copyright Dapper Labs, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package old_parser + +import ( + "fmt" + "math" + "math/big" + "math/rand" + "strconv" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/onflow/cadence/runtime/ast" + "github.com/onflow/cadence/runtime/common" + "github.com/onflow/cadence/runtime/errors" + "github.com/onflow/cadence/runtime/parser/lexer" + "github.com/onflow/cadence/runtime/tests/utils" +) + +func TestParseSimpleInfixExpression(t *testing.T) { + + t.Parallel() + + t.Run("no spaces", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression("1+2*3") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.BinaryExpression{ + Operation: ast.OperationPlus, + Left: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + Right: &ast.BinaryExpression{ + Operation: ast.OperationMul, + Left: &ast.IntegerExpression{ + PositiveLiteral: []byte("2"), + Value: big.NewInt(2), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + EndPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + }, + }, + Right: &ast.IntegerExpression{ + PositiveLiteral: []byte("3"), + Value: big.NewInt(3), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + EndPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + }, + }, + }, + }, + result, + ) + }) + + t.Run("with spaces", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression(" 1 + 2 * 3 ") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.BinaryExpression{ + Operation: ast.OperationPlus, + Left: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + EndPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + }, + }, + Right: &ast.BinaryExpression{ + Operation: ast.OperationMul, + Left: &ast.IntegerExpression{ + PositiveLiteral: []byte("2"), + Value: big.NewInt(2), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 10, Offset: 10}, + EndPos: ast.Position{Line: 1, Column: 10, Offset: 10}, + }, + }, + Right: &ast.IntegerExpression{ + PositiveLiteral: []byte("3"), + Value: big.NewInt(3), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 17, Offset: 17}, + EndPos: ast.Position{Line: 1, Column: 17, Offset: 17}, + }, + }, + }, + }, + result, + ) + }) + + t.Run("repeated infix, same operator, left associative", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression("1 + 2 + 3") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.BinaryExpression{ + Operation: ast.OperationPlus, + Left: &ast.BinaryExpression{ + Operation: ast.OperationPlus, + Left: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + Right: &ast.IntegerExpression{ + PositiveLiteral: []byte("2"), + Value: big.NewInt(2), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + EndPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + }, + }, + }, + Right: &ast.IntegerExpression{ + PositiveLiteral: []byte("3"), + Value: big.NewInt(3), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 8, Offset: 8}, + EndPos: ast.Position{Line: 1, Column: 8, Offset: 8}, + }, + }, + }, + result, + ) + }) + + t.Run("repeated infix, same operator, right associative", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression("1 ?? 2 ?? 3") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.BinaryExpression{ + Operation: ast.OperationNilCoalesce, + Left: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + Right: &ast.BinaryExpression{ + Operation: ast.OperationNilCoalesce, + Left: &ast.IntegerExpression{ + PositiveLiteral: []byte("2"), + Value: big.NewInt(2), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 5, Offset: 5}, + EndPos: ast.Position{Line: 1, Column: 5, Offset: 5}, + }, + }, + Right: &ast.IntegerExpression{ + PositiveLiteral: []byte("3"), + Value: big.NewInt(3), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 10, Offset: 10}, + EndPos: ast.Position{Line: 1, Column: 10, Offset: 10}, + }, + }, + }, + }, + result, + ) + }) +} + +func TestParseAdvancedExpression(t *testing.T) { + + t.Parallel() + + t.Run("mixed infix and prefix", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression("1 +- 2 -- 3") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.BinaryExpression{ + Operation: ast.OperationMinus, + Left: &ast.BinaryExpression{ + Operation: ast.OperationPlus, + Left: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + Right: &ast.IntegerExpression{ + PositiveLiteral: []byte("2"), + Value: big.NewInt(-2), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 3, Offset: 3}, + EndPos: ast.Position{Line: 1, Column: 5, Offset: 5}, + }, + }, + }, + Right: &ast.IntegerExpression{ + PositiveLiteral: []byte("3"), + Value: big.NewInt(-3), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 8, Offset: 8}, + EndPos: ast.Position{Line: 1, Column: 10, Offset: 10}, + }, + }, + }, + result, + ) + }) + + t.Run("nested expression", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression("(1 + 2) * 3") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.BinaryExpression{ + Operation: ast.OperationMul, + Left: &ast.BinaryExpression{ + Operation: ast.OperationPlus, + Left: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + EndPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + }, + }, + Right: &ast.IntegerExpression{ + PositiveLiteral: []byte("2"), + Value: big.NewInt(2), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 5, Offset: 5}, + EndPos: ast.Position{Line: 1, Column: 5, Offset: 5}, + }, + }, + }, + Right: &ast.IntegerExpression{ + PositiveLiteral: []byte("3"), + Value: big.NewInt(3), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 10, Offset: 10}, + EndPos: ast.Position{Line: 1, Column: 10, Offset: 10}, + }, + }, + }, + result, + ) + }) + + t.Run("MemoryError in setExprMetaLeftDenotation", func(t *testing.T) { + + t.Parallel() + + gauge := makeLimitingMemoryGauge() + gauge.debug = true + gauge.Limit(common.MemoryKindPosition, 11) + + var panicMsg any + (func() { + defer func() { + panicMsg = recover() + }() + ParseExpression(gauge, []byte("1 < 2"), Config{}) + })() + + require.IsType(t, errors.MemoryError{}, panicMsg) + + fatalError, _ := panicMsg.(errors.MemoryError) + var expectedError limitingMemoryGaugeError + assert.ErrorAs(t, fatalError, &expectedError) + }) + + t.Run("MemoryError in parser.report", func(t *testing.T) { + + t.Parallel() + + gauge := makeLimitingMemoryGauge() + gauge.Limit(common.MemoryKindIntegerExpression, 1) + + var panicMsg any + (func() { + defer func() { + panicMsg = recover() + }() + + ParseExpression(gauge, []byte("1 < 2 > 3"), Config{}) + })() + + require.IsType(t, errors.MemoryError{}, panicMsg) + + fatalError, _ := panicMsg.(errors.MemoryError) + var expectedError limitingMemoryGaugeError + assert.ErrorAs(t, fatalError, &expectedError) + }) + + t.Run("less and greater", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression("1 < 2 > 3") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.BinaryExpression{ + Operation: ast.OperationGreater, + Left: &ast.BinaryExpression{ + Operation: ast.OperationLess, + Left: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + Right: &ast.IntegerExpression{ + PositiveLiteral: []byte("2"), + Value: big.NewInt(2), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + EndPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + }, + }, + }, + Right: &ast.IntegerExpression{ + PositiveLiteral: []byte("3"), + Value: big.NewInt(3), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 8, Offset: 8}, + EndPos: ast.Position{Line: 1, Column: 8, Offset: 8}, + }, + }, + }, + result, + ) + }) + + t.Run("conditional", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression("a ? b : c ? d : e") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.ConditionalExpression{ + Test: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "a", + Pos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + Then: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "b", + Pos: ast.Position{Line: 1, Column: 4, Offset: 4}, + }, + }, + Else: &ast.ConditionalExpression{ + Test: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "c", + Pos: ast.Position{Line: 1, Column: 8, Offset: 8}, + }, + }, + Then: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "d", + Pos: ast.Position{Line: 1, Column: 12, Offset: 12}, + }, + }, + Else: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "e", + Pos: ast.Position{Line: 1, Column: 16, Offset: 16}, + }, + }, + }, + }, + result, + ) + }) + + t.Run("boolean expressions", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression("true + false") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.BinaryExpression{ + Operation: ast.OperationPlus, + Left: &ast.BoolExpression{ + Value: true, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 3, Offset: 3}, + }, + }, + Right: &ast.BoolExpression{ + Value: false, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 7, Offset: 7}, + EndPos: ast.Position{Line: 1, Column: 11, Offset: 11}, + }, + }, + }, + result, + ) + }) + + t.Run("move operator, nested", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression("(<-x)") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.UnaryExpression{ + Operation: ast.OperationMove, + Expression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "x", + Pos: ast.Position{Line: 1, Column: 3, Offset: 3}, + }, + }, + StartPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + }, + result, + ) + }) + +} + +func TestParseArrayExpression(t *testing.T) { + + t.Parallel() + + t.Run("single line", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression("[ 1,2 + 3, 4 , 5 ]") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.ArrayExpression{ + Values: []ast.Expression{ + &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + EndPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + }, + }, + &ast.BinaryExpression{ + Operation: ast.OperationPlus, + Left: &ast.IntegerExpression{ + PositiveLiteral: []byte("2"), + Value: big.NewInt(2), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + EndPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + }, + }, + Right: &ast.IntegerExpression{ + PositiveLiteral: []byte("3"), + Value: big.NewInt(3), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 8, Offset: 8}, + EndPos: ast.Position{Line: 1, Column: 8, Offset: 8}, + }, + }, + }, + &ast.IntegerExpression{ + PositiveLiteral: []byte("4"), + Value: big.NewInt(4), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 11, Offset: 11}, + EndPos: ast.Position{Line: 1, Column: 11, Offset: 11}, + }, + }, + &ast.IntegerExpression{ + PositiveLiteral: []byte("5"), + Value: big.NewInt(5), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 17, Offset: 17}, + EndPos: ast.Position{Line: 1, Column: 17, Offset: 17}, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 19, Offset: 19}, + }, + }, + result, + ) + }) + + t.Run("multi line", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression("[ 1 , \n 2 \n , \n\n 3 \n\n\n]") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.ArrayExpression{ + Values: []ast.Expression{ + &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + EndPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + }, + }, + &ast.IntegerExpression{ + PositiveLiteral: []byte("2"), + Value: big.NewInt(2), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 2, Column: 1, Offset: 8}, + EndPos: ast.Position{Line: 2, Column: 1, Offset: 8}, + }, + }, + &ast.IntegerExpression{ + PositiveLiteral: []byte("3"), + Value: big.NewInt(3), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 5, Column: 1, Offset: 17}, + EndPos: ast.Position{Line: 5, Column: 1, Offset: 17}, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 8, Column: 0, Offset: 22}, + }, + }, + result, + ) + }) + + t.Run("empty, separated by newline", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression("[\n]") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.ArrayExpression{ + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 2, Column: 0, Offset: 2}, + }, + }, + result, + ) + }) + +} + +func TestParseDictionaryExpression(t *testing.T) { + + t.Parallel() + + t.Run("single line", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression("{ 1:2 + 3, 4 : 5 }") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.DictionaryExpression{ + Entries: []ast.DictionaryEntry{ + { + Key: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + EndPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + }, + }, + Value: &ast.BinaryExpression{ + Operation: ast.OperationPlus, + Left: &ast.IntegerExpression{ + PositiveLiteral: []byte("2"), + Value: big.NewInt(2), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + EndPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + }, + }, + Right: &ast.IntegerExpression{ + PositiveLiteral: []byte("3"), + Value: big.NewInt(3), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 8, Offset: 8}, + EndPos: ast.Position{Line: 1, Column: 8, Offset: 8}, + }, + }, + }, + }, + { + Key: &ast.IntegerExpression{ + PositiveLiteral: []byte("4"), + Value: big.NewInt(4), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 11, Offset: 11}, + EndPos: ast.Position{Line: 1, Column: 11, Offset: 11}, + }, + }, + Value: &ast.IntegerExpression{ + PositiveLiteral: []byte("5"), + Value: big.NewInt(5), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 17, Offset: 17}, + EndPos: ast.Position{Line: 1, Column: 17, Offset: 17}, + }, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 19, Offset: 19}, + }, + }, + result, + ) + }) + + t.Run("multi line", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression("{ 1 : 2 , \n 3 \n : \n 4 \n , \n\n 5 \n\n : \n\n 6 \n\n }") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.DictionaryExpression{ + Entries: []ast.DictionaryEntry{ + { + Key: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + EndPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + }, + }, + Value: &ast.IntegerExpression{ + PositiveLiteral: []byte("2"), + Value: big.NewInt(2), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 6, Offset: 6}, + EndPos: ast.Position{Line: 1, Column: 6, Offset: 6}, + }, + }, + }, + { + Key: &ast.IntegerExpression{ + PositiveLiteral: []byte("3"), + Value: big.NewInt(3), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 2, Column: 1, Offset: 12}, + EndPos: ast.Position{Line: 2, Column: 1, Offset: 12}, + }, + }, + Value: &ast.IntegerExpression{ + PositiveLiteral: []byte("4"), + Value: big.NewInt(4), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 4, Column: 1, Offset: 20}, + EndPos: ast.Position{Line: 4, Column: 1, Offset: 20}, + }, + }, + }, + { + Key: &ast.IntegerExpression{ + PositiveLiteral: []byte("5"), + Value: big.NewInt(5), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 7, Column: 1, Offset: 29}, + EndPos: ast.Position{Line: 7, Column: 1, Offset: 29}, + }, + }, + Value: &ast.IntegerExpression{ + PositiveLiteral: []byte("6"), + Value: big.NewInt(6), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 11, Column: 1, Offset: 39}, + EndPos: ast.Position{Line: 11, Column: 1, Offset: 39}, + }, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 13, Column: 1, Offset: 44}, + }, + }, + result, + ) + }) + + t.Run("empty, separated by newline", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression("{\n}") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.DictionaryExpression{ + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 2, Column: 0, Offset: 2}, + }, + }, + result, + ) + }) +} + +func TestParseIndexExpression(t *testing.T) { + t.Parallel() + + t.Run("index expression", func(t *testing.T) { + t.Parallel() + + result, errs := testParseExpression("a[0]") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.IndexExpression{ + TargetExpression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "a", + Pos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + IndexingExpression: &ast.IntegerExpression{ + PositiveLiteral: []byte("0"), + Value: new(big.Int), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + EndPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + EndPos: ast.Position{Line: 1, Column: 3, Offset: 3}, + }, + }, + result, + ) + }) + + t.Run("index expression with whitespace", func(t *testing.T) { + t.Parallel() + + result, errs := testParseExpression("a [ 0 ]") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.IndexExpression{ + TargetExpression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "a", + Pos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + IndexingExpression: &ast.IntegerExpression{ + PositiveLiteral: []byte("0"), + Value: new(big.Int), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + EndPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + EndPos: ast.Position{Line: 1, Column: 6, Offset: 6}, + }, + }, + result, + ) + }) + + t.Run("index expression with identifier", func(t *testing.T) { + t.Parallel() + + result, errs := testParseExpression("a [foo]") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.IndexExpression{ + TargetExpression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "a", + Pos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + IndexingExpression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "foo", + Pos: ast.Position{Line: 1, Column: 3, Offset: 3}, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + EndPos: ast.Position{Line: 1, Column: 6, Offset: 6}, + }, + }, + result, + ) + }) +} + +func TestParseIdentifier(t *testing.T) { + + t.Parallel() + + t.Run("identifier in addition", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression("a + 3") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.BinaryExpression{ + Operation: ast.OperationPlus, + Left: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "a", + Pos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + Right: &ast.IntegerExpression{ + PositiveLiteral: []byte("3"), + Value: big.NewInt(3), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + EndPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + }, + }, + }, + result, + ) + }) +} + +func TestParsePath(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression("/foo/bar") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.PathExpression{ + Domain: ast.Identifier{ + Identifier: "foo", + Pos: ast.Position{Line: 1, Column: 1, Offset: 1}, + }, + Identifier: ast.Identifier{ + Identifier: "bar", + Pos: ast.Position{Line: 1, Column: 5, Offset: 5}, + }, + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + result, + ) +} + +func TestParseString(t *testing.T) { + + t.Parallel() + + t.Run("valid, empty", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression("\"\"") + assert.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.StringExpression{ + Value: "", + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + }, + }, + result, + ) + }) + + t.Run("invalid, empty, missing end at end of file", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression("\"") + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "invalid end of string literal: missing '\"'", + Pos: ast.Position{Offset: 1, Line: 1, Column: 1}, + }, + }, + errs, + ) + + utils.AssertEqualWithDiff(t, + &ast.StringExpression{ + Value: "", + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + result, + ) + }) + + t.Run("invalid, empty, missing end at end of line", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression("\"\n") + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "invalid end of string literal: missing '\"'", + Pos: ast.Position{Line: 1, Column: 1, Offset: 1}, + }, + }, + errs, + ) + + utils.AssertEqualWithDiff(t, + &ast.StringExpression{ + Value: "", + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + result, + ) + }) + + t.Run("invalid, non-empty, missing end at end of file", func(t *testing.T) { + + t.Parallel() + result, errs := testParseExpression("\"t") + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "invalid end of string literal: missing '\"'", + Pos: ast.Position{Offset: 2, Line: 1, Column: 2}, + }, + }, + errs, + ) + + utils.AssertEqualWithDiff(t, + &ast.StringExpression{ + Value: "t", + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + }, + }, + result, + ) + }) + + t.Run("invalid, non-empty, missing end at end of line", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression("\"t\n") + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "invalid end of string literal: missing '\"'", + Pos: ast.Position{Line: 1, Column: 2, Offset: 2}, + }, + }, + errs, + ) + + utils.AssertEqualWithDiff(t, + &ast.StringExpression{ + Value: "t", + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + }, + }, + result, + ) + }) + + t.Run("invalid, non-empty, missing escape character", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression("\"\\") + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "incomplete escape sequence: missing character after escape character", + Pos: ast.Position{Offset: 2, Line: 1, Column: 2}, + }, + &SyntaxError{ + Message: "invalid end of string literal: missing '\"'", + Pos: ast.Position{Offset: 2, Line: 1, Column: 2}, + }, + }, + errs, + ) + + utils.AssertEqualWithDiff(t, + &ast.StringExpression{ + Value: "", + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + }, + }, + result, + ) + }) + + t.Run("valid, with escapes", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression(`"te\tst\"te\u{1F3CE}\u{FE0F}xt"`) + assert.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.StringExpression{ + Value: "te\tst\"te\U0001F3CE\uFE0Fxt", + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 30, Offset: 30}, + }, + }, + result, + ) + }) + + t.Run("invalid, unknown escape character", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression(`"te\Xst"`) + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "invalid escape character: 'X'", + Pos: ast.Position{Offset: 8, Line: 1, Column: 8}, + }, + }, + errs, + ) + + utils.AssertEqualWithDiff(t, + &ast.StringExpression{ + Value: "test", + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 7, Offset: 7}, + }, + }, + result, + ) + }) + + t.Run("invalid, missing '{' after Unicode escape character", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression(`"te\u`) + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "incomplete Unicode escape sequence: missing character '{' after escape character", + Pos: ast.Position{Offset: 5, Line: 1, Column: 5}, + }, + &SyntaxError{ + Message: "invalid end of string literal: missing '\"'", + Pos: ast.Position{Offset: 5, Line: 1, Column: 5}, + }, + }, + errs, + ) + + utils.AssertEqualWithDiff(t, + &ast.StringExpression{ + Value: "te", + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + }, + }, + result, + ) + }) + + t.Run("invalid, invalid character after Unicode escape character", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression(`"te\us`) + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "invalid Unicode escape sequence: expected '{', got 's'", + Pos: ast.Position{Offset: 6, Line: 1, Column: 6}, + }, + &SyntaxError{ + Message: "invalid end of string literal: missing '\"'", + Pos: ast.Position{Offset: 6, Line: 1, Column: 6}, + }, + }, + errs, + ) + + utils.AssertEqualWithDiff(t, + &ast.StringExpression{ + Value: "te", + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 5, Offset: 5}, + }, + }, + result, + ) + }) + + t.Run("invalid, missing '}' after Unicode escape sequence digits", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression(`"te\u{`) + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "incomplete Unicode escape sequence: missing character '}' after escape character", + Pos: ast.Position{Offset: 6, Line: 1, Column: 6}, + }, + &SyntaxError{ + Message: "invalid end of string literal: missing '\"'", + Pos: ast.Position{Offset: 6, Line: 1, Column: 6}, + }, + }, + errs, + ) + + utils.AssertEqualWithDiff(t, + &ast.StringExpression{ + Value: "te", + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 5, Offset: 5}, + }, + }, + result, + ) + }) + + t.Run("valid, empty Unicode escape sequence", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression(`"te\u{}"`) + assert.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.StringExpression{ + Value: "te", + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 7, Offset: 7}, + }, + }, + result, + ) + }) + + t.Run("valid, non-empty Unicode escape sequence", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression( + `"te\u{73}t ` + + `\u{4A}J\u{4a}J ` + + `\u{4B}K\u{4b}K ` + + `\u{4C}L\u{4c}L ` + + `\u{4D}M\u{4d}M ` + + `\u{4E}N\u{4e}N ` + + `\u{4F}O\u{4f}O"`, + ) + assert.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.StringExpression{ + Value: "test JJJJ KKKK LLLL MMMM NNNN OOOO", + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 100, Offset: 100}, + }, + }, + result, + ) + }) + + t.Run("invalid, non-empty Unicode escape sequence", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression(`"te\u{X}st"`) + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "invalid Unicode escape sequence: expected hex digit, got 'X'", + Pos: ast.Position{Offset: 11, Line: 1, Column: 11}, + }, + }, + errs, + ) + + utils.AssertEqualWithDiff(t, + &ast.StringExpression{ + Value: "test", + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 10, Offset: 10}, + }, + }, + result, + ) + }) +} + +func TestParseInvocation(t *testing.T) { + + t.Parallel() + + t.Run("no arguments", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression("f()") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.InvocationExpression{ + InvokedExpression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "f", + Pos: ast.Position{Offset: 0, Line: 1, Column: 0}, + }, + }, + Arguments: nil, + ArgumentsStartPos: ast.Position{Offset: 1, Line: 1, Column: 1}, + EndPos: ast.Position{Offset: 2, Line: 1, Column: 2}, + }, + result, + ) + }) + + t.Run("no arguments, with whitespace", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression("f ()") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.InvocationExpression{ + InvokedExpression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "f", + Pos: ast.Position{Offset: 0, Line: 1, Column: 0}, + }, + }, + Arguments: nil, + ArgumentsStartPos: ast.Position{Offset: 2, Line: 1, Column: 2}, + EndPos: ast.Position{Offset: 3, Line: 1, Column: 3}, + }, + result, + ) + }) + + t.Run("no arguments, with whitespace within params", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression("f ( )") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.InvocationExpression{ + InvokedExpression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "f", + Pos: ast.Position{Offset: 0, Line: 1, Column: 0}, + }, + }, + Arguments: nil, + ArgumentsStartPos: ast.Position{Offset: 2, Line: 1, Column: 2}, + EndPos: ast.Position{Offset: 4, Line: 1, Column: 4}, + }, + result, + ) + }) + + t.Run("with arguments", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression("f(1)") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.InvocationExpression{ + InvokedExpression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "f", + Pos: ast.Position{Offset: 0, Line: 1, Column: 0}, + }, + }, + Arguments: []*ast.Argument{ + { + Label: "", + Expression: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 2, Line: 1, Column: 2}, + EndPos: ast.Position{Offset: 2, Line: 1, Column: 2}, + }, + }, + TrailingSeparatorPos: ast.Position{Offset: 3, Line: 1, Column: 3}, + }, + }, + ArgumentsStartPos: ast.Position{Offset: 1, Line: 1, Column: 1}, + EndPos: ast.Position{Offset: 3, Line: 1, Column: 3}, + }, + result, + ) + }) + + t.Run("with labeled arguments", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression("f(label:1)") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.InvocationExpression{ + InvokedExpression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "f", + Pos: ast.Position{Offset: 0, Line: 1, Column: 0}, + }, + }, + Arguments: []*ast.Argument{ + { + Label: "label", + LabelStartPos: &ast.Position{Offset: 2, Line: 1, Column: 2}, + LabelEndPos: &ast.Position{Offset: 7, Line: 1, Column: 7}, + Expression: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 8, Line: 1, Column: 8}, + EndPos: ast.Position{Offset: 8, Line: 1, Column: 8}, + }, + }, + TrailingSeparatorPos: ast.Position{Offset: 9, Line: 1, Column: 9}, + }, + }, + ArgumentsStartPos: ast.Position{Offset: 1, Line: 1, Column: 1}, + EndPos: ast.Position{Offset: 9, Line: 1, Column: 9}, + }, + result, + ) + }) + + t.Run("with arguments, multiple", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression("f(1,2)") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.InvocationExpression{ + InvokedExpression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "f", + Pos: ast.Position{Offset: 0, Line: 1, Column: 0}, + }, + }, + Arguments: []*ast.Argument{ + { + Label: "", + Expression: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 2, Line: 1, Column: 2}, + EndPos: ast.Position{Offset: 2, Line: 1, Column: 2}, + }, + }, + TrailingSeparatorPos: ast.Position{Offset: 3, Line: 1, Column: 3}, + }, + { + Label: "", + Expression: &ast.IntegerExpression{ + PositiveLiteral: []byte("2"), + Value: big.NewInt(2), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 4, Line: 1, Column: 4}, + EndPos: ast.Position{Offset: 4, Line: 1, Column: 4}, + }, + }, + TrailingSeparatorPos: ast.Position{Offset: 5, Line: 1, Column: 5}, + }, + }, + ArgumentsStartPos: ast.Position{Offset: 1, Line: 1, Column: 1}, + EndPos: ast.Position{Offset: 5, Line: 1, Column: 5}, + }, + result, + ) + }) + + t.Run("with arguments, multiple, labeled", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression("f(a:1,b:2)") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.InvocationExpression{ + InvokedExpression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "f", + Pos: ast.Position{Offset: 0, Line: 1, Column: 0}, + }, + }, + Arguments: []*ast.Argument{ + { + Label: "a", + LabelStartPos: &ast.Position{Offset: 2, Line: 1, Column: 2}, + LabelEndPos: &ast.Position{Offset: 3, Line: 1, Column: 3}, + Expression: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 4, Line: 1, Column: 4}, + EndPos: ast.Position{Offset: 4, Line: 1, Column: 4}, + }, + }, + TrailingSeparatorPos: ast.Position{Offset: 5, Line: 1, Column: 5}, + }, + { + Label: "b", + LabelStartPos: &ast.Position{Offset: 6, Line: 1, Column: 6}, + LabelEndPos: &ast.Position{Offset: 7, Line: 1, Column: 7}, + Expression: &ast.IntegerExpression{ + PositiveLiteral: []byte("2"), + Value: big.NewInt(2), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 8, Line: 1, Column: 8}, + EndPos: ast.Position{Offset: 8, Line: 1, Column: 8}, + }, + }, + TrailingSeparatorPos: ast.Position{Offset: 9, Line: 1, Column: 9}, + }, + }, + ArgumentsStartPos: ast.Position{Offset: 1, Line: 1, Column: 1}, + EndPos: ast.Position{Offset: 9, Line: 1, Column: 9}, + }, + result, + ) + }) + + t.Run("invalid: no arguments, multiple commas", func(t *testing.T) { + + t.Parallel() + + _, errs := testParseExpression("f(,,)") + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "expected argument or end of argument list, got ','", + Pos: ast.Position{Offset: 2, Line: 1, Column: 2}, + }, + }, + errs, + ) + }) + + t.Run("invalid: with argument, multiple commas", func(t *testing.T) { + + t.Parallel() + + _, errs := testParseExpression("f(1,,)") + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "expected argument or end of argument list, got ','", + Pos: ast.Position{Offset: 4, Line: 1, Column: 4}, + }, + }, + errs, + ) + }) + + t.Run("invalid: with multiple argument, no commas", func(t *testing.T) { + + t.Parallel() + + _, errs := testParseExpression("f(1 2)") + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "unexpected argument in argument list (expecting delimiter or end of argument list)," + + " got decimal integer", + Pos: ast.Position{Offset: 4, Line: 1, Column: 4}, + }, + }, + errs, + ) + }) + + t.Run("with arguments, nested", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression("f(1,g(2))") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.InvocationExpression{ + InvokedExpression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "f", + Pos: ast.Position{Offset: 0, Line: 1, Column: 0}, + }, + }, + Arguments: []*ast.Argument{ + { + Label: "", + Expression: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 2, Line: 1, Column: 2}, + EndPos: ast.Position{Offset: 2, Line: 1, Column: 2}, + }, + }, + TrailingSeparatorPos: ast.Position{Offset: 3, Line: 1, Column: 3}, + }, + { + Label: "", + Expression: &ast.InvocationExpression{ + InvokedExpression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "g", + Pos: ast.Position{Offset: 4, Line: 1, Column: 4}, + }, + }, + Arguments: []*ast.Argument{ + { + Label: "", + Expression: &ast.IntegerExpression{ + PositiveLiteral: []byte("2"), + Value: big.NewInt(2), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 6, Line: 1, Column: 6}, + EndPos: ast.Position{Offset: 6, Line: 1, Column: 6}, + }, + }, + TrailingSeparatorPos: ast.Position{Offset: 7, Line: 1, Column: 7}, + }, + }, + ArgumentsStartPos: ast.Position{Offset: 5, Line: 1, Column: 5}, + EndPos: ast.Position{Offset: 7, Line: 1, Column: 7}, + }, + TrailingSeparatorPos: ast.Position{Offset: 8, Line: 1, Column: 8}, + }, + }, + ArgumentsStartPos: ast.Position{Offset: 1, Line: 1, Column: 1}, + EndPos: ast.Position{Offset: 8, Line: 1, Column: 8}, + }, + result, + ) + }) + + t.Run("with arguments, nested, string", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression("f(1,g(\"test\"))") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.InvocationExpression{ + InvokedExpression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "f", + Pos: ast.Position{Offset: 0, Line: 1, Column: 0}, + }, + }, + Arguments: []*ast.Argument{ + { + Label: "", + Expression: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 2, Line: 1, Column: 2}, + EndPos: ast.Position{Offset: 2, Line: 1, Column: 2}, + }, + }, + TrailingSeparatorPos: ast.Position{Offset: 3, Line: 1, Column: 3}, + }, + { + Label: "", + Expression: &ast.InvocationExpression{ + InvokedExpression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "g", + Pos: ast.Position{Offset: 4, Line: 1, Column: 4}, + }, + }, + Arguments: []*ast.Argument{ + { + Label: "", + Expression: &ast.StringExpression{ + Value: "test", + Range: ast.Range{ + StartPos: ast.Position{Offset: 6, Line: 1, Column: 6}, + EndPos: ast.Position{Offset: 11, Line: 1, Column: 11}, + }, + }, + TrailingSeparatorPos: ast.Position{Offset: 12, Line: 1, Column: 12}, + }, + }, + ArgumentsStartPos: ast.Position{Offset: 5, Line: 1, Column: 5}, + EndPos: ast.Position{Offset: 12, Line: 1, Column: 12}, + }, + TrailingSeparatorPos: ast.Position{Offset: 13, Line: 1, Column: 13}, + }, + }, + ArgumentsStartPos: ast.Position{Offset: 1, Line: 1, Column: 1}, + EndPos: ast.Position{Offset: 13, Line: 1, Column: 13}, + }, + result, + ) + }) +} + +func TestParseMemberExpression(t *testing.T) { + + t.Parallel() + + t.Run("identifier, no space", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression("f.n") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.MemberExpression{ + Expression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "f", + Pos: ast.Position{Offset: 0, Line: 1, Column: 0}, + }, + }, + AccessPos: ast.Position{Offset: 1, Line: 1, Column: 1}, + Identifier: ast.Identifier{ + Identifier: "n", + Pos: ast.Position{Offset: 2, Line: 1, Column: 2}, + }, + }, + result, + ) + }) + + t.Run("whitespace before", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression("f .n") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.MemberExpression{ + Expression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "f", + Pos: ast.Position{Offset: 0, Line: 1, Column: 0}, + }, + }, + AccessPos: ast.Position{Offset: 2, Line: 1, Column: 2}, + Identifier: ast.Identifier{ + Identifier: "n", + Pos: ast.Position{Offset: 3, Line: 1, Column: 3}, + }, + }, + result, + ) + }) + + t.Run("missing name", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression("f.") + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "expected member name, got EOF", + Pos: ast.Position{Offset: 2, Line: 1, Column: 2}, + }, + }, + errs, + ) + + utils.AssertEqualWithDiff(t, + &ast.MemberExpression{ + Expression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "f", + Pos: ast.Position{Offset: 0, Line: 1, Column: 0}, + }, + }, + AccessPos: ast.Position{Offset: 1, Line: 1, Column: 1}, + }, + result, + ) + }) + + t.Run("precedence, left", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression("f.n * 3") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.BinaryExpression{ + Operation: ast.OperationMul, + Left: &ast.MemberExpression{ + Expression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "f", + Pos: ast.Position{Offset: 0, Line: 1, Column: 0}, + }, + }, + AccessPos: ast.Position{Offset: 1, Line: 1, Column: 1}, + Identifier: ast.Identifier{ + Identifier: "n", + Pos: ast.Position{Offset: 2, Line: 1, Column: 2}, + }, + }, + Right: &ast.IntegerExpression{ + PositiveLiteral: []byte("3"), + Value: big.NewInt(3), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 6, Line: 1, Column: 6}, + EndPos: ast.Position{Offset: 6, Line: 1, Column: 6}, + }, + }, + }, + result, + ) + }) + + t.Run("precedence, right", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression("3 * f.n") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.BinaryExpression{ + Operation: ast.OperationMul, + Left: &ast.IntegerExpression{ + PositiveLiteral: []byte("3"), + Value: big.NewInt(3), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 0, Line: 1, Column: 0}, + EndPos: ast.Position{Offset: 0, Line: 1, Column: 0}, + }, + }, + Right: &ast.MemberExpression{ + Expression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "f", + Pos: ast.Position{Offset: 4, Line: 1, Column: 4}, + }, + }, + AccessPos: ast.Position{Offset: 5, Line: 1, Column: 5}, + Identifier: ast.Identifier{ + Identifier: "n", + Pos: ast.Position{Offset: 6, Line: 1, Column: 6}, + }, + }, + }, + result, + ) + }) + + t.Run("identifier, optional", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression("f?.n") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.MemberExpression{ + Optional: true, + Expression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "f", + Pos: ast.Position{Offset: 0, Line: 1, Column: 0}, + }, + }, + AccessPos: ast.Position{Offset: 2, Line: 1, Column: 2}, + Identifier: ast.Identifier{ + Identifier: "n", + Pos: ast.Position{Offset: 3, Line: 1, Column: 3}, + }, + }, + result, + ) + }) + + t.Run("in declaration", func(t *testing.T) { + + t.Parallel() + + const code = ` + let a = b.c + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.VariableDeclaration{ + Access: ast.AccessNotSpecified, + IsConstant: true, + Identifier: ast.Identifier{ + Identifier: "a", + Pos: ast.Position{Offset: 15, Line: 2, Column: 14}, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationCopy, + Pos: ast.Position{Offset: 17, Line: 2, Column: 16}, + }, + Value: &ast.MemberExpression{ + Expression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "b", + Pos: ast.Position{Offset: 19, Line: 2, Column: 18}, + }, + }, + Identifier: ast.Identifier{ + Identifier: "c", + Pos: ast.Position{Offset: 21, Line: 2, Column: 20}, + }, + AccessPos: ast.Position{Offset: 20, Line: 2, Column: 19}, + }, + StartPos: ast.Position{Offset: 11, Line: 2, Column: 10}, + }, + }, + result.Declarations(), + ) + }) +} + +func TestParseBlockComment(t *testing.T) { + + t.Parallel() + + t.Run("nested", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression(" /* test foo/* bar */ asd*/ true") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.BoolExpression{ + Value: true, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 30, Offset: 30}, + EndPos: ast.Position{Line: 1, Column: 33, Offset: 33}, + }, + }, + result, + ) + }) + + t.Run("two comments", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression(" /*test foo*/ /* bar */ true") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.BoolExpression{ + Value: true, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 26, Offset: 26}, + EndPos: ast.Position{Line: 1, Column: 29, Offset: 29}, + }, + }, + result, + ) + }) + + t.Run("in infix", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression(" 1/*test foo*/+/* bar */ 2 ") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.BinaryExpression{ + Operation: ast.OperationPlus, + Left: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + EndPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + }, + }, + Right: &ast.IntegerExpression{ + PositiveLiteral: []byte("2"), + Value: big.NewInt(2), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 27, Offset: 27}, + EndPos: ast.Position{Line: 1, Column: 27, Offset: 27}, + }, + }, + }, + result, + ) + }) + + t.Run("nested, extra closing", func(t *testing.T) { + + t.Parallel() + + _, errs := testParseExpression(" /* test foo/* bar */ asd*/ true */ bar") + utils.AssertEqualWithDiff(t, + []error{ + // `true */ bar` is parsed as infix operation of path + &SyntaxError{ + Message: "expected token identifier", + Pos: ast.Position{ + Offset: 37, + Line: 1, + Column: 37, + }, + }, + }, + errs, + ) + }) + + t.Run("nested, missing closing", func(t *testing.T) { + + t.Parallel() + + _, errs := testParseExpression(" /* test foo/* bar */ asd true ") + utils.AssertEqualWithDiff(t, + []error{ + // `true */ bar` is parsed as infix operation of path + &SyntaxError{ + Message: "missing comment end '*/'", + Pos: ast.Position{ + Offset: 33, + Line: 1, + Column: 33, + }, + }, + &SyntaxError{ + Message: "unexpected end of program", + Pos: ast.Position{ + Offset: 33, + Line: 1, + Column: 33, + }, + }, + }, + errs, + ) + }) + + t.Run("invalid content", func(t *testing.T) { + + t.Parallel() + + // The lexer should never produce such an invalid token stream in the first place + + tokens := &testTokenStream{ + tokens: []lexer.Token{ + { + Type: lexer.TokenBlockCommentStart, + Range: ast.Range{ + StartPos: ast.Position{ + Line: 1, + Offset: 0, + Column: 0, + }, + EndPos: ast.Position{ + Line: 1, + Offset: 1, + Column: 1, + }, + }, + }, + { + Type: lexer.TokenIdentifier, + Range: ast.Range{ + StartPos: ast.Position{ + Line: 1, + Offset: 2, + Column: 2, + }, + EndPos: ast.Position{ + Line: 1, + Offset: 4, + Column: 4, + }, + }, + }, + {Type: lexer.TokenEOF}, + }, + input: []byte(`/*foo`), + } + + _, errs := ParseTokenStream( + nil, + tokens, + func(p *parser) (ast.Expression, error) { + return parseExpression(p, lowestBindingPower) + }, + Config{}, + ) + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "unexpected token identifier in block comment", + Pos: ast.Position{ + Line: 1, + Offset: 2, + Column: 2, + }, + }, + }, + errs, + ) + }) +} + +func BenchmarkParseInfix(b *testing.B) { + + for i := 0; i < b.N; i++ { + _, errs := testParseExpression("(8 - 1 + 3) * 6 - ((3 + 7) * 2)") + if len(errs) > 0 { + b.Fatalf("parsing expression failed: %s", errs) + } + } +} + +func BenchmarkParseArray(b *testing.B) { + + var builder strings.Builder + for i := 0; i < 10_000; i++ { + if i > 0 { + builder.WriteString(", ") + } + builder.WriteString(strconv.Itoa(rand.Intn(math.MaxUint8))) + } + + lit := fmt.Sprintf(`[%s]`, builder.String()) + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + _, errs := testParseExpression(lit) + if len(errs) > 0 { + b.Fatalf("parsing expression failed: %s", errs) + } + } +} + +func TestParseReference(t *testing.T) { + + t.Parallel() + + t.Run("valid", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression("& t as T") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.ReferenceExpression{ + Expression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "t", + Pos: ast.Position{Line: 1, Column: 2, Offset: 2}, + }, + }, + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + result, + ) + }) + + t.Run("invalid: missing casting expression", func(t *testing.T) { + + t.Parallel() + + const code = `&y[z]` + + _, errs := testParseExpression(code) + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "expected casting expression", + Pos: ast.Position{ + Offset: 5, + Line: 1, + Column: 5, + }, + }, + }, + errs, + ) + }) + + t.Run("invalid: optional referenced value", func(t *testing.T) { + + t.Parallel() + + const code = `&x[y]? as &Z?` + + _, errs := testParseExpression(code) + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "expected casting expression", + Pos: ast.Position{ + Offset: 5, + Line: 1, + Column: 5, + }, + }, + }, + errs, + ) + }) +} + +func TestParseNilCoelesceReference(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression(` + &xs["a"] as &Int? ?? 1 + `) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.BinaryExpression{ + Operation: ast.OperationNilCoalesce, + Left: &ast.ReferenceExpression{ + Expression: &ast.IndexExpression{ + TargetExpression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "xs", + Pos: ast.Position{ + Offset: 12, + Line: 2, + Column: 11, + }, + }, + }, + IndexingExpression: &ast.StringExpression{ + Value: "a", + Range: ast.Range{ + StartPos: ast.Position{ + Offset: 15, + Line: 2, + Column: 14, + }, + EndPos: ast.Position{ + Offset: 17, + Line: 2, + Column: 16, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{ + Offset: 14, + Line: 2, + Column: 13, + }, + EndPos: ast.Position{ + Offset: 18, + Line: 2, + Column: 17, + }, + }, + }, + StartPos: ast.Position{ + Offset: 11, + Line: 2, + Column: 10, + }, + }, + Right: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{ + Offset: 32, + Line: 2, + Column: 31, + }, + EndPos: ast.Position{ + Offset: 32, + Line: 2, + Column: 31, + }, + }, + }, + }, + result, + ) +} + +func TestParseCasts(t *testing.T) { + + t.Parallel() + + t.Run("non-failable", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression(" t as T") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.CastingExpression{ + Operation: ast.OperationCast, + Expression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "t", + Pos: ast.Position{Line: 1, Column: 1, Offset: 1}, + }, + }, + TypeAnnotation: &ast.TypeAnnotation{ + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "T", + Pos: ast.Position{Line: 1, Column: 6, Offset: 6}, + }, + }, + StartPos: ast.Position{Line: 1, Column: 6, Offset: 6}, + }, + }, + result, + ) + }) + + t.Run("failable", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression(" t as? T") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.CastingExpression{ + Operation: ast.OperationFailableCast, + Expression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "t", + Pos: ast.Position{Line: 1, Column: 1, Offset: 1}, + }, + }, + TypeAnnotation: &ast.TypeAnnotation{ + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "T", + Pos: ast.Position{Line: 1, Column: 7, Offset: 7}, + }, + }, + StartPos: ast.Position{Line: 1, Column: 7, Offset: 7}, + }, + }, + result, + ) + + }) + + t.Run("force", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression(" t as! T") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.CastingExpression{ + Operation: ast.OperationForceCast, + Expression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "t", + Pos: ast.Position{Line: 1, Column: 1, Offset: 1}, + }, + }, + TypeAnnotation: &ast.TypeAnnotation{ + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "T", + Pos: ast.Position{Line: 1, Column: 7, Offset: 7}, + }, + }, + StartPos: ast.Position{Line: 1, Column: 7, Offset: 7}, + }, + }, + result, + ) + }) +} + +func TestParseForceExpression(t *testing.T) { + + t.Parallel() + + t.Run("identifier", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression("t!") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.ForceExpression{ + Expression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "t", + Pos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + EndPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + }, + result, + ) + }) + + t.Run("with whitespace", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression(" t ! ") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.ForceExpression{ + Expression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "t", + Pos: ast.Position{Line: 1, Column: 1, Offset: 1}, + }, + }, + EndPos: ast.Position{Line: 1, Column: 3, Offset: 3}, + }, + result, + ) + }) + + t.Run("precedence, force unwrap before move", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression("<-t!") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.UnaryExpression{ + Operation: ast.OperationMove, + Expression: &ast.ForceExpression{ + Expression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "t", + Pos: ast.Position{Line: 1, Column: 2, Offset: 2}, + }, + }, + EndPos: ast.Position{Line: 1, Column: 3, Offset: 3}, + }, + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + result, + ) + }) + + t.Run("precedence", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression("10 * t!") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.BinaryExpression{ + Operation: ast.OperationMul, + Left: &ast.IntegerExpression{ + PositiveLiteral: []byte("10"), + Value: big.NewInt(10), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + }, + }, + Right: &ast.ForceExpression{ + Expression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "t", + Pos: ast.Position{Line: 1, Column: 6, Offset: 6}, + }, + }, + EndPos: ast.Position{Line: 1, Column: 7, Offset: 7}, + }}, + result, + ) + }) + + t.Run("newline", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseStatements("x\n!y") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Statement{ + &ast.ExpressionStatement{ + Expression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "x", + Pos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + }, + &ast.ExpressionStatement{ + Expression: &ast.UnaryExpression{ + Operation: ast.OperationNegate, + Expression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "y", + Pos: ast.Position{Line: 2, Column: 1, Offset: 3}, + }, + }, + StartPos: ast.Position{Line: 2, Column: 0, Offset: 2}, + }, + }, + }, + result, + ) + }) + + t.Run("member access, newline", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseStatements("x\n.y!") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Statement{ + &ast.ExpressionStatement{ + Expression: &ast.ForceExpression{ + Expression: &ast.MemberExpression{ + Expression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "x", + Pos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + AccessPos: ast.Position{Line: 2, Column: 0, Offset: 2}, + Identifier: ast.Identifier{ + Identifier: "y", + Pos: ast.Position{Line: 2, Column: 1, Offset: 3}, + }, + }, + EndPos: ast.Position{Line: 2, Column: 2, Offset: 4}, + }, + }, + }, + result, + ) + }) + + t.Run("member access, whitespace", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseStatements("x. y") + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "invalid whitespace after '.'", + Pos: ast.Position{Offset: 2, Line: 1, Column: 2}, + }, + }, + errs, + ) + + utils.AssertEqualWithDiff(t, + []ast.Statement{ + &ast.ExpressionStatement{ + Expression: &ast.MemberExpression{ + Expression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "x", + Pos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + AccessPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + Identifier: ast.Identifier{ + Identifier: "y", + Pos: ast.Position{Line: 1, Column: 3, Offset: 3}, + }, + }, + }, + }, + result, + ) + }) +} + +func TestParseCreate(t *testing.T) { + + t.Parallel() + + t.Run("simple", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression("create T()") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.CreateExpression{ + InvocationExpression: &ast.InvocationExpression{ + InvokedExpression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "T", + Pos: ast.Position{Line: 1, Column: 7, Offset: 7}, + }, + }, + ArgumentsStartPos: ast.Position{Line: 1, Column: 8, Offset: 8}, + EndPos: ast.Position{Line: 1, Column: 9, Offset: 9}, + }, + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + result, + ) + }) +} + +func TestParseNil(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression(" nil") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.NilExpression{ + Pos: ast.Position{Line: 1, Column: 1, Offset: 1}, + }, + result, + ) +} + +func TestParseDestroy(t *testing.T) { + + t.Parallel() + + t.Run("simple", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression("destroy t") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.DestroyExpression{ + Expression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "t", + Pos: ast.Position{Line: 1, Column: 8, Offset: 8}, + }, + }, + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + result, + ) + }) +} + +func TestParseAttach(t *testing.T) { + + t.Parallel() + + t.Run("simple", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression("attach E() to r") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.AttachExpression{ + Base: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "r", + Pos: ast.Position{Line: 1, Column: 14, Offset: 14}, + }, + }, + Attachment: &ast.InvocationExpression{ + InvokedExpression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "E", + Pos: ast.Position{Line: 1, Column: 7, Offset: 7}, + }, + }, + ArgumentsStartPos: ast.Position{Line: 1, Column: 8, Offset: 8}, + EndPos: ast.Position{Line: 1, Column: 9, Offset: 9}, + }, + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + result, + ) + }) + + t.Run("non-invocation", func(t *testing.T) { + + t.Parallel() + + _, errs := testParseExpression("attach A to E") + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "expected token '('", + Pos: ast.Position{Offset: 9, Line: 1, Column: 9}, + }, + }, + errs, + ) + }) + + t.Run("nested", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression("attach A() to attach B() to r") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.AttachExpression{ + Base: &ast.AttachExpression{ + Base: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "r", + Pos: ast.Position{Line: 1, Column: 28, Offset: 28}, + }, + }, + Attachment: &ast.InvocationExpression{ + InvokedExpression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "B", + Pos: ast.Position{Line: 1, Column: 21, Offset: 21}, + }, + }, + ArgumentsStartPos: ast.Position{Line: 1, Column: 22, Offset: 22}, + EndPos: ast.Position{Line: 1, Column: 23, Offset: 23}, + }, + StartPos: ast.Position{Line: 1, Column: 14, Offset: 14}, + }, + Attachment: &ast.InvocationExpression{ + InvokedExpression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "A", + Pos: ast.Position{Line: 1, Column: 7, Offset: 7}, + }, + }, + ArgumentsStartPos: ast.Position{Line: 1, Column: 8, Offset: 8}, + EndPos: ast.Position{Line: 1, Column: 9, Offset: 9}, + }, + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + result, + ) + }) + + t.Run("missing to", func(t *testing.T) { + + t.Parallel() + + _, errs := testParseExpression("attach A()") + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "expected 'to', got EOF", + Pos: ast.Position{Offset: 10, Line: 1, Column: 10}, + }, + }, + errs, + ) + }) + + t.Run("missing base", func(t *testing.T) { + + t.Parallel() + + _, errs := testParseExpression("attach E() to") + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "unexpected end of program", + Pos: ast.Position{Offset: 13, Line: 1, Column: 13}, + }, + }, + errs, + ) + }) +} + +func TestParseLineComment(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression(" //// // this is a comment\n 1 / 2") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.BinaryExpression{ + Operation: ast.OperationDiv, + Left: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 2, Column: 1, Offset: 28}, + EndPos: ast.Position{Line: 2, Column: 1, Offset: 28}, + }, + }, + Right: &ast.IntegerExpression{ + PositiveLiteral: []byte("2"), + Value: big.NewInt(2), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 2, Column: 5, Offset: 32}, + EndPos: ast.Position{Line: 2, Column: 5, Offset: 32}, + }, + }, + }, + result, + ) +} + +func TestParseFunctionExpression(t *testing.T) { + + t.Parallel() + + t.Run("without return type", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression("fun () { }") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.FunctionExpression{ + ParameterList: &ast.ParameterList{ + Parameters: nil, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + EndPos: ast.Position{Line: 1, Column: 5, Offset: 5}, + }, + }, + FunctionBlock: &ast.FunctionBlock{ + Block: &ast.Block{ + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 7, Offset: 7}, + EndPos: ast.Position{Line: 1, Column: 9, Offset: 9}, + }, + }, + }, + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + result, + ) + }) + + t.Run("with return type", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression("fun (): X { }") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.FunctionExpression{ + ParameterList: &ast.ParameterList{ + Parameters: nil, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + EndPos: ast.Position{Line: 1, Column: 5, Offset: 5}, + }, + }, + ReturnTypeAnnotation: &ast.TypeAnnotation{ + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "X", + Pos: ast.Position{Line: 1, Column: 8, Offset: 8}, + }, + }, + StartPos: ast.Position{Line: 1, Column: 8, Offset: 8}, + }, + FunctionBlock: &ast.FunctionBlock{ + Block: &ast.Block{ + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 10, Offset: 10}, + EndPos: ast.Position{Line: 1, Column: 12, Offset: 12}, + }, + }, + }, + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + result, + ) + }) +} + +func TestParseIntegerLiterals(t *testing.T) { + + t.Parallel() + + t.Run("binary prefix, missing trailing digits", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression(`0b`) + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "missing digits", + Pos: ast.Position{Offset: 1, Line: 1, Column: 1}, + }, + &InvalidIntegerLiteralError{ + Literal: "0b", + IntegerLiteralKind: common.IntegerLiteralKindBinary, + InvalidIntegerLiteralKind: InvalidNumberLiteralKindMissingDigits, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + }, + }, + }, + errs, + ) + + utils.AssertEqualWithDiff(t, + &ast.IntegerExpression{ + PositiveLiteral: []byte("0b"), + Value: new(big.Int), + Base: 2, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + }, + }, + result, + ) + }) + + t.Run("binary", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression(`0b101010`) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.IntegerExpression{ + PositiveLiteral: []byte("0b101010"), + Value: big.NewInt(42), + Base: 2, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 7, Offset: 7}, + }, + }, + result, + ) + }) + + t.Run("binary with leading zeros", func(t *testing.T) { + + result, errs := testParseExpression(`0b001000`) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.IntegerExpression{ + PositiveLiteral: []byte("0b001000"), + Value: big.NewInt(8), + Base: 2, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 7, Offset: 7}, + }, + }, + result, + ) + }) + + t.Run("binary with underscores", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression(`0b101010_101010`) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.IntegerExpression{ + PositiveLiteral: []byte("0b101010_101010"), + Value: big.NewInt(2730), + Base: 2, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 14, Offset: 14}, + }, + }, + result, + ) + }) + + t.Run("binary with leading underscore", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression(`0b_101010_101010`) + utils.AssertEqualWithDiff(t, + []error{ + &InvalidIntegerLiteralError{ + Literal: "0b_101010_101010", + IntegerLiteralKind: common.IntegerLiteralKindBinary, + InvalidIntegerLiteralKind: InvalidNumberLiteralKindLeadingUnderscore, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 15, Offset: 15}, + }, + }, + }, + errs, + ) + + utils.AssertEqualWithDiff(t, + &ast.IntegerExpression{ + PositiveLiteral: []byte("0b_101010_101010"), + Value: big.NewInt(2730), + Base: 2, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 15, Offset: 15}, + }, + }, + result, + ) + }) + + t.Run("binary with trailing underscore", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression(`0b101010_101010_`) + utils.AssertEqualWithDiff(t, + []error{ + &InvalidIntegerLiteralError{ + Literal: "0b101010_101010_", + IntegerLiteralKind: common.IntegerLiteralKindBinary, + InvalidIntegerLiteralKind: InvalidNumberLiteralKindTrailingUnderscore, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 15, Offset: 15}, + }, + }, + }, + errs, + ) + + utils.AssertEqualWithDiff(t, + &ast.IntegerExpression{ + PositiveLiteral: []byte("0b101010_101010_"), + Value: big.NewInt(2730), + Base: 2, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 15, Offset: 15}, + }, + }, + result, + ) + }) + + t.Run("octal prefix, missing trailing digits", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression(`0o`) + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "missing digits", + Pos: ast.Position{Line: 1, Column: 1, Offset: 1}, + }, + &InvalidIntegerLiteralError{ + Literal: `0o`, + IntegerLiteralKind: common.IntegerLiteralKindOctal, + InvalidIntegerLiteralKind: InvalidNumberLiteralKindMissingDigits, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + }, + }, + }, + errs, + ) + + utils.AssertEqualWithDiff(t, + &ast.IntegerExpression{ + PositiveLiteral: []byte("0o"), + Value: new(big.Int), + Base: 8, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + }, + }, + result, + ) + }) + + t.Run("octal", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression(`0o32`) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.IntegerExpression{ + PositiveLiteral: []byte("0o32"), + Value: big.NewInt(26), + Base: 8, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 3, Offset: 3}, + }, + }, + result, + ) + }) + + t.Run("octal with underscores", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression(`0o32_45`) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.IntegerExpression{ + PositiveLiteral: []byte("0o32_45"), + Value: big.NewInt(1701), + Base: 8, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 6, Offset: 6}, + }, + }, + result, + ) + }) + + t.Run("octal with trailing underscore", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression(`0o_32_45`) + utils.AssertEqualWithDiff(t, + []error{ + &InvalidIntegerLiteralError{ + Literal: "0o_32_45", + IntegerLiteralKind: common.IntegerLiteralKindOctal, + InvalidIntegerLiteralKind: InvalidNumberLiteralKindLeadingUnderscore, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 7, Offset: 7}, + }, + }, + }, + errs, + ) + + utils.AssertEqualWithDiff(t, + &ast.IntegerExpression{ + PositiveLiteral: []byte("0o_32_45"), + Value: big.NewInt(1701), + Base: 8, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 7, Offset: 7}, + }, + }, + result, + ) + }) + + t.Run("octal with leading underscore", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression(`0o32_45_`) + utils.AssertEqualWithDiff(t, + []error{ + &InvalidIntegerLiteralError{ + Literal: "0o32_45_", + IntegerLiteralKind: common.IntegerLiteralKindOctal, + InvalidIntegerLiteralKind: InvalidNumberLiteralKindTrailingUnderscore, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 7, Offset: 7}, + }, + }, + }, + errs, + ) + + utils.AssertEqualWithDiff(t, + &ast.IntegerExpression{ + PositiveLiteral: []byte("0o32_45_"), + Value: big.NewInt(1701), + Base: 8, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 7, Offset: 7}, + }, + }, + result, + ) + }) + + t.Run("decimal", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression(`1234567890`) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.IntegerExpression{ + PositiveLiteral: []byte("1234567890"), + Value: big.NewInt(1234567890), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 9, Offset: 9}, + }, + }, + result, + ) + }) + + t.Run("decimal with underscores", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression(`1_234_567_890`) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.IntegerExpression{ + PositiveLiteral: []byte("1_234_567_890"), + Value: big.NewInt(1234567890), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 12, Offset: 12}, + }, + }, + result, + ) + }) + + t.Run("decimal with trailing underscore", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression(`1_234_567_890_`) + utils.AssertEqualWithDiff(t, + []error{ + &InvalidIntegerLiteralError{ + Literal: "1_234_567_890_", + IntegerLiteralKind: common.IntegerLiteralKindDecimal, + InvalidIntegerLiteralKind: InvalidNumberLiteralKindTrailingUnderscore, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 13, Offset: 13}, + }, + }, + }, + errs, + ) + + utils.AssertEqualWithDiff(t, + &ast.IntegerExpression{ + PositiveLiteral: []byte("1_234_567_890_"), + Value: big.NewInt(1234567890), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 13, Offset: 13}, + }, + }, + result, + ) + }) + + t.Run("hexadecimal prefix, missing trailing digits", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression(`0x`) + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "missing digits", + Pos: ast.Position{Line: 1, Column: 1, Offset: 1}, + }, + &InvalidIntegerLiteralError{ + Literal: `0x`, + IntegerLiteralKind: common.IntegerLiteralKindHexadecimal, + InvalidIntegerLiteralKind: InvalidNumberLiteralKindMissingDigits, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + }, + }, + }, + errs, + ) + + utils.AssertEqualWithDiff(t, + &ast.IntegerExpression{ + PositiveLiteral: []byte("0x"), + Value: new(big.Int), + Base: 16, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + }, + }, + result, + ) + }) + + t.Run("hexadecimal", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression(`0xf2`) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.IntegerExpression{ + PositiveLiteral: []byte("0xf2"), + Value: big.NewInt(242), + Base: 16, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 3, Offset: 3}, + }, + }, + result, + ) + }) + + t.Run("hexadecimal with underscores", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression(`0xf2_09`) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.IntegerExpression{ + PositiveLiteral: []byte("0xf2_09"), + Value: big.NewInt(61961), + Base: 16, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 6, Offset: 6}, + }, + }, + result, + ) + }) + + t.Run("hexadecimal with leading underscore", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression(`0x_f2_09`) + utils.AssertEqualWithDiff(t, + []error{ + &InvalidIntegerLiteralError{ + Literal: "0x_f2_09", + IntegerLiteralKind: common.IntegerLiteralKindHexadecimal, + InvalidIntegerLiteralKind: InvalidNumberLiteralKindLeadingUnderscore, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 7, Offset: 7}, + }, + }, + }, + errs, + ) + + utils.AssertEqualWithDiff(t, + &ast.IntegerExpression{ + PositiveLiteral: []byte("0x_f2_09"), + Value: big.NewInt(61961), + Base: 16, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 7, Offset: 7}, + }, + }, + result, + ) + }) + + t.Run("hexadecimal with trailing underscore", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression(`0xf2_09_`) + utils.AssertEqualWithDiff(t, + []error{ + &InvalidIntegerLiteralError{ + Literal: `0xf2_09_`, + IntegerLiteralKind: common.IntegerLiteralKindHexadecimal, + InvalidIntegerLiteralKind: InvalidNumberLiteralKindTrailingUnderscore, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 7, Offset: 7}, + }, + }, + }, + errs, + ) + + utils.AssertEqualWithDiff(t, + &ast.IntegerExpression{ + PositiveLiteral: []byte("0xf2_09_"), + Value: big.NewInt(61961), + Base: 16, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 7, Offset: 7}, + }, + }, + result, + ) + }) + + t.Run("0", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression(`0`) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.IntegerExpression{ + PositiveLiteral: []byte("0"), + Value: big.NewInt(0), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + result, + ) + }) + + t.Run("01", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression(`01`) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.IntegerExpression{ + PositiveLiteral: []byte("01"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + }, + }, + result, + ) + }) + + t.Run("09", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression(`09`) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.IntegerExpression{ + PositiveLiteral: []byte("09"), + Value: big.NewInt(9), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + }, + }, + result, + ) + }) + + t.Run("leading zeros", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression("00123") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.IntegerExpression{ + PositiveLiteral: []byte("00123"), + Value: big.NewInt(123), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + }, + }, + result, + ) + }) + + t.Run("invalid prefix", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression(`0z123`) + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "invalid number literal prefix: 'z'", + Pos: ast.Position{Line: 1, Column: 1, Offset: 1}, + }, + &InvalidIntegerLiteralError{ + Literal: `0z123`, + IntegerLiteralKind: common.IntegerLiteralKindUnknown, + InvalidIntegerLiteralKind: InvalidNumberLiteralKindUnknownPrefix, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + }, + }, + }, + errs, + ) + + utils.AssertEqualWithDiff(t, + &ast.IntegerExpression{ + PositiveLiteral: []byte("0z123"), + Value: new(big.Int), + Base: 1, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + }, + }, + result, + ) + }) + + t.Run("leading zero and underscore", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression(`0_100`) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.IntegerExpression{ + PositiveLiteral: []byte("0_100"), + Value: big.NewInt(100), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + }, + }, + result, + ) + }) + + t.Run("leading one and underscore", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression(`1_100`) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.IntegerExpression{ + PositiveLiteral: []byte("1_100"), + Value: big.NewInt(1100), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + }, + }, + result, + ) + }) + + t.Run("leading underscore", func(t *testing.T) { + + t.Parallel() + + // NOTE: a leading underscore introduces an identifier + + result, errs := testParseExpression(`_100`) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "_100", + Pos: ast.Position{Offset: 0, Line: 1, Column: 0}, + }, + }, + result, + ) + }) +} + +func TestParseFixedPoint(t *testing.T) { + + t.Parallel() + + t.Run("with underscores", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression("1234_5678_90.0009_8765_4321") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.FixedPointExpression{ + PositiveLiteral: []byte("1234_5678_90.0009_8765_4321"), + Negative: false, + UnsignedInteger: big.NewInt(1234567890), + Fractional: big.NewInt(987654321), + Scale: 12, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 26, Offset: 26}, + }, + }, + result, + ) + }) + + t.Run("leading zero", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression("0.1") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.FixedPointExpression{ + PositiveLiteral: []byte("0.1"), + Negative: false, + UnsignedInteger: big.NewInt(0), + Fractional: big.NewInt(1), + Scale: 1, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + }, + }, + result, + ) + }) + + t.Run("missing fractional digits", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression("0.") + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "missing fractional digits", + Pos: ast.Position{Line: 1, Column: 1, Offset: 1}, + }, + }, + errs, + ) + + utils.AssertEqualWithDiff(t, + &ast.FixedPointExpression{ + PositiveLiteral: []byte("0."), + Negative: false, + UnsignedInteger: big.NewInt(0), + Fractional: big.NewInt(0), + Scale: 1, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + }, + }, + result, + ) + }) +} + +func TestParseLessThanOrTypeArguments(t *testing.T) { + + t.Parallel() + + t.Run("binary expression with less operator", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression("1 < 2") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.BinaryExpression{ + Operation: ast.OperationLess, + Left: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + Right: &ast.IntegerExpression{ + PositiveLiteral: []byte("2"), + Value: big.NewInt(2), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + EndPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + }, + }, + }, + result, + ) + }) + + t.Run("invocation, zero type arguments, zero arguments", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression("a < > ()") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.InvocationExpression{ + InvokedExpression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "a", + Pos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + TypeArguments: nil, + Arguments: nil, + ArgumentsStartPos: ast.Position{Line: 1, Column: 6, Offset: 6}, + EndPos: ast.Position{Line: 1, Column: 7, Offset: 7}, + }, + result, + ) + }) + + t.Run("invocation, one type argument, one argument", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression("a < { K : V } > ( 1 )") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.InvocationExpression{ + InvokedExpression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "a", + Pos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + TypeArguments: []*ast.TypeAnnotation{ + { + IsResource: false, + Type: &ast.DictionaryType{ + KeyType: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "K", + Pos: ast.Position{Line: 1, Column: 6, Offset: 6}, + }, + }, + ValueType: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "V", + Pos: ast.Position{Line: 1, Column: 10, Offset: 10}, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + EndPos: ast.Position{Line: 1, Column: 12, Offset: 12}, + }, + }, + StartPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + }, + }, + Arguments: []*ast.Argument{ + { + Expression: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 18, Offset: 18}, + EndPos: ast.Position{Line: 1, Column: 18, Offset: 18}, + }, + }, + TrailingSeparatorPos: ast.Position{Line: 1, Column: 20, Offset: 20}, + }, + }, + ArgumentsStartPos: ast.Position{Line: 1, Column: 16, Offset: 16}, + EndPos: ast.Position{Line: 1, Column: 20, Offset: 20}, + }, + result, + ) + }) + + t.Run("invocation, three type arguments, two arguments", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression("a < { K : V } , @R , [ S ] > ( 1 , 2 )") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.InvocationExpression{ + InvokedExpression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "a", + Pos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + TypeArguments: []*ast.TypeAnnotation{ + { + IsResource: false, + Type: &ast.DictionaryType{ + KeyType: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "K", + Pos: ast.Position{Line: 1, Column: 6, Offset: 6}, + }, + }, + ValueType: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "V", + Pos: ast.Position{Line: 1, Column: 10, Offset: 10}, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + EndPos: ast.Position{Line: 1, Column: 12, Offset: 12}, + }, + }, + StartPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + }, + { + IsResource: true, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "R", + Pos: ast.Position{Line: 1, Column: 17, Offset: 17}, + }, + }, + StartPos: ast.Position{Line: 1, Column: 16, Offset: 16}, + }, + { + IsResource: false, + Type: &ast.VariableSizedType{ + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "S", + Pos: ast.Position{Line: 1, Column: 23, Offset: 23}, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 21, Offset: 21}, + EndPos: ast.Position{Line: 1, Column: 25, Offset: 25}, + }, + }, + StartPos: ast.Position{Line: 1, Column: 21, Offset: 21}, + }, + }, + Arguments: []*ast.Argument{ + { + Expression: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 31, Offset: 31}, + EndPos: ast.Position{Line: 1, Column: 31, Offset: 31}, + }, + }, + TrailingSeparatorPos: ast.Position{Line: 1, Column: 33, Offset: 33}, + }, + { + Expression: &ast.IntegerExpression{ + PositiveLiteral: []byte("2"), + Value: big.NewInt(2), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 35, Offset: 35}, + EndPos: ast.Position{Line: 1, Column: 35, Offset: 35}, + }, + }, + TrailingSeparatorPos: ast.Position{Line: 1, Column: 37, Offset: 37}, + }, + }, + ArgumentsStartPos: ast.Position{Line: 1, Column: 29, Offset: 29}, + EndPos: ast.Position{Line: 1, Column: 37, Offset: 37}, + }, + result, + ) + }) + + t.Run("precedence, invocation in binary expression", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression("1 + a<>()") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.BinaryExpression{ + Operation: ast.OperationPlus, + Left: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + Right: &ast.InvocationExpression{ + InvokedExpression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "a", + Pos: ast.Position{Line: 1, Column: 4, Offset: 4}, + }, + }, + TypeArguments: nil, + Arguments: nil, + ArgumentsStartPos: ast.Position{Line: 1, Column: 7, Offset: 7}, + EndPos: ast.Position{Line: 1, Column: 8, Offset: 8}, + }, + }, + result, + ) + }) + + t.Run("invocation, one type argument, nested type, no spaces", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression("a>()") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.InvocationExpression{ + InvokedExpression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "a", + Pos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + TypeArguments: []*ast.TypeAnnotation{ + { + IsResource: false, + Type: &ast.InstantiationType{ + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "T", + Pos: ast.Position{Line: 1, Column: 2, Offset: 2}, + }, + }, + TypeArguments: []*ast.TypeAnnotation{ + { + IsResource: false, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "U", + Pos: ast.Position{Line: 1, Column: 4, Offset: 4}, + }, + }, + StartPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + }, + }, + TypeArgumentsStartPos: ast.Position{Line: 1, Column: 3, Offset: 3}, + EndPos: ast.Position{Line: 1, Column: 5, Offset: 5}, + }, + StartPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + }, + }, + ArgumentsStartPos: ast.Position{Line: 1, Column: 7, Offset: 7}, + EndPos: ast.Position{Line: 1, Column: 8, Offset: 8}, + }, + result, + ) + }) + + t.Run("invocation, one type argument, nested type, spaces", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression("a >()") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.InvocationExpression{ + InvokedExpression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "a", + Pos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + TypeArguments: []*ast.TypeAnnotation{ + { + IsResource: false, + Type: &ast.InstantiationType{ + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "T", + Pos: ast.Position{Line: 1, Column: 2, Offset: 2}, + }, + }, + TypeArguments: []*ast.TypeAnnotation{ + { + IsResource: false, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "U", + Pos: ast.Position{Line: 1, Column: 5, Offset: 5}, + }, + }, + StartPos: ast.Position{Line: 1, Column: 5, Offset: 5}, + }, + }, + TypeArgumentsStartPos: ast.Position{Line: 1, Column: 3, Offset: 3}, + EndPos: ast.Position{Line: 1, Column: 7, Offset: 7}, + }, + StartPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + }, + }, + ArgumentsStartPos: ast.Position{Line: 1, Column: 10, Offset: 10}, + EndPos: ast.Position{Line: 1, Column: 11, Offset: 11}, + }, + result, + ) + }) + + t.Run("precedence, binary expressions, less than", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression("0 + 1 < 2") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.BinaryExpression{ + Operation: ast.OperationLess, + Left: &ast.BinaryExpression{ + Operation: ast.OperationPlus, + Left: &ast.IntegerExpression{ + PositiveLiteral: []byte("0"), + Value: new(big.Int), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + Right: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + EndPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + }, + }, + }, + Right: &ast.IntegerExpression{ + PositiveLiteral: []byte("2"), + Value: big.NewInt(2), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 8, Offset: 8}, + EndPos: ast.Position{Line: 1, Column: 8, Offset: 8}, + }, + }, + }, + result, + ) + }) + + t.Run("precedence, binary expressions, left shift", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression("0 + 1 << 2") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.BinaryExpression{ + Operation: ast.OperationBitwiseLeftShift, + Left: &ast.BinaryExpression{ + Operation: ast.OperationPlus, + Left: &ast.IntegerExpression{ + PositiveLiteral: []byte("0"), + Value: new(big.Int), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + Right: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + EndPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + }, + }, + }, + Right: &ast.IntegerExpression{ + PositiveLiteral: []byte("2"), + Value: big.NewInt(2), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 9, Offset: 9}, + EndPos: ast.Position{Line: 1, Column: 9, Offset: 9}, + }, + }, + }, + result, + ) + }) + + t.Run("precedence, binary expressions, greater than", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression("0 + 1 > 2") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.BinaryExpression{ + Operation: ast.OperationGreater, + Left: &ast.BinaryExpression{ + Operation: ast.OperationPlus, + Left: &ast.IntegerExpression{ + PositiveLiteral: []byte("0"), + Value: new(big.Int), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + Right: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + EndPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + }, + }, + }, + Right: &ast.IntegerExpression{ + PositiveLiteral: []byte("2"), + Value: big.NewInt(2), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 8, Offset: 8}, + EndPos: ast.Position{Line: 1, Column: 8, Offset: 8}, + }, + }, + }, + result, + ) + }) + + t.Run("precedence, binary expressions, right shift", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseExpression("0 + 1 >> 2") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.BinaryExpression{ + Operation: ast.OperationBitwiseRightShift, + Left: &ast.BinaryExpression{ + Operation: ast.OperationPlus, + Left: &ast.IntegerExpression{ + PositiveLiteral: []byte("0"), + Value: new(big.Int), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + Right: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + EndPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + }, + }, + }, + Right: &ast.IntegerExpression{ + PositiveLiteral: []byte("2"), + Value: big.NewInt(2), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 9, Offset: 9}, + EndPos: ast.Position{Line: 1, Column: 9, Offset: 9}, + }, + }, + }, + result, + ) + }) +} + +func TestParseBoolExpression(t *testing.T) { + + t.Parallel() + + const code = ` + let a = true + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.VariableDeclaration{ + Access: ast.AccessNotSpecified, + IsConstant: true, + Identifier: ast.Identifier{ + Identifier: "a", + Pos: ast.Position{Offset: 10, Line: 2, Column: 9}, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationCopy, + Pos: ast.Position{Offset: 12, Line: 2, Column: 11}, + }, + Value: &ast.BoolExpression{ + Value: true, + Range: ast.Range{ + StartPos: ast.Position{Offset: 14, Line: 2, Column: 13}, + EndPos: ast.Position{Offset: 17, Line: 2, Column: 16}, + }, + }, + StartPos: ast.Position{Offset: 6, Line: 2, Column: 5}, + }, + }, + result.Declarations(), + ) +} + +func TestParseIdentifierExpression(t *testing.T) { + + t.Parallel() + + const code = ` + let b = a + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.VariableDeclaration{ + Access: ast.AccessNotSpecified, + IsConstant: true, + Identifier: ast.Identifier{ + Identifier: "b", + Pos: ast.Position{Offset: 10, Line: 2, Column: 9}, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationCopy, + Pos: ast.Position{Offset: 12, Line: 2, Column: 11}, + }, + Value: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "a", + Pos: ast.Position{Offset: 14, Line: 2, Column: 13}, + }, + }, + StartPos: ast.Position{Offset: 6, Line: 2, Column: 5}, + }, + }, + result.Declarations(), + ) +} + +func TestParseArrayExpressionInVariableDeclaration(t *testing.T) { + + t.Parallel() + + const code = ` + let a = [1, 2] + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.VariableDeclaration{ + Access: ast.AccessNotSpecified, + IsConstant: true, + Identifier: ast.Identifier{Identifier: "a", + Pos: ast.Position{Offset: 10, Line: 2, Column: 9}, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationCopy, + Pos: ast.Position{Offset: 12, Line: 2, Column: 11}, + }, + Value: &ast.ArrayExpression{ + Values: []ast.Expression{ + &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 15, Line: 2, Column: 14}, + EndPos: ast.Position{Offset: 15, Line: 2, Column: 14}, + }, + }, + &ast.IntegerExpression{ + PositiveLiteral: []byte("2"), + Value: big.NewInt(2), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 18, Line: 2, Column: 17}, + EndPos: ast.Position{Offset: 18, Line: 2, Column: 17}, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 14, Line: 2, Column: 13}, + EndPos: ast.Position{Offset: 19, Line: 2, Column: 18}, + }, + }, + StartPos: ast.Position{Offset: 6, Line: 2, Column: 5}, + }, + }, + result.Declarations(), + ) +} + +func TestParseDictionaryExpressionInVariableDeclaration(t *testing.T) { + + t.Parallel() + + const code = ` + let x = {"a": 1, "b": 2} + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.VariableDeclaration{ + Access: ast.AccessNotSpecified, + IsConstant: true, + Identifier: ast.Identifier{Identifier: "x", + Pos: ast.Position{Offset: 10, Line: 2, Column: 9}, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationCopy, + Pos: ast.Position{Offset: 12, Line: 2, Column: 11}, + }, + Value: &ast.DictionaryExpression{ + Entries: []ast.DictionaryEntry{ + { + Key: &ast.StringExpression{ + Value: "a", + Range: ast.Range{ + StartPos: ast.Position{Offset: 15, Line: 2, Column: 14}, + EndPos: ast.Position{Offset: 17, Line: 2, Column: 16}, + }, + }, + Value: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 20, Line: 2, Column: 19}, + EndPos: ast.Position{Offset: 20, Line: 2, Column: 19}, + }, + }, + }, + { + Key: &ast.StringExpression{ + Value: "b", + Range: ast.Range{ + StartPos: ast.Position{Offset: 23, Line: 2, Column: 22}, + EndPos: ast.Position{Offset: 25, Line: 2, Column: 24}, + }, + }, + Value: &ast.IntegerExpression{ + PositiveLiteral: []byte("2"), + Value: big.NewInt(2), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 28, Line: 2, Column: 27}, + EndPos: ast.Position{Offset: 28, Line: 2, Column: 27}, + }, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 14, Line: 2, Column: 13}, + EndPos: ast.Position{Offset: 29, Line: 2, Column: 28}, + }, + }, + StartPos: ast.Position{Offset: 6, Line: 2, Column: 5}, + }, + }, + result.Declarations(), + ) +} + +func TestParseInvocationExpressionWithoutLabels(t *testing.T) { + + t.Parallel() + + const code = ` + let a = b(1, 2) + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.VariableDeclaration{ + Access: ast.AccessNotSpecified, + IsConstant: true, + Identifier: ast.Identifier{ + Identifier: "a", + Pos: ast.Position{Offset: 10, Line: 2, Column: 9}, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationCopy, + Pos: ast.Position{Offset: 12, Line: 2, Column: 11}, + }, + Value: &ast.InvocationExpression{ + InvokedExpression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "b", + Pos: ast.Position{Offset: 14, Line: 2, Column: 13}, + }, + }, + Arguments: []*ast.Argument{ + { + Label: "", + Expression: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 16, Line: 2, Column: 15}, + EndPos: ast.Position{Offset: 16, Line: 2, Column: 15}, + }, + }, + TrailingSeparatorPos: ast.Position{Offset: 17, Line: 2, Column: 16}, + }, + { + Label: "", + Expression: &ast.IntegerExpression{ + PositiveLiteral: []byte("2"), + Value: big.NewInt(2), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 19, Line: 2, Column: 18}, + EndPos: ast.Position{Offset: 19, Line: 2, Column: 18}, + }, + }, + TrailingSeparatorPos: ast.Position{Offset: 20, Line: 2, Column: 19}, + }, + }, + ArgumentsStartPos: ast.Position{Offset: 15, Line: 2, Column: 14}, + EndPos: ast.Position{Offset: 20, Line: 2, Column: 19}, + }, + StartPos: ast.Position{Offset: 6, Line: 2, Column: 5}, + }, + }, + result.Declarations(), + ) +} + +func TestParseInvocationExpressionWithLabels(t *testing.T) { + + t.Parallel() + + const code = ` + let a = b(x: 1, y: 2, z : 3) + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.VariableDeclaration{ + Access: ast.AccessNotSpecified, + IsConstant: true, + Identifier: ast.Identifier{ + Identifier: "a", + Pos: ast.Position{Offset: 10, Line: 2, Column: 9}, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationCopy, + Pos: ast.Position{Offset: 12, Line: 2, Column: 11}, + }, + Value: &ast.InvocationExpression{ + InvokedExpression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "b", + Pos: ast.Position{Offset: 14, Line: 2, Column: 13}, + }, + }, + Arguments: []*ast.Argument{ + { + Label: "x", + LabelStartPos: &ast.Position{Offset: 16, Line: 2, Column: 15}, + LabelEndPos: &ast.Position{Offset: 17, Line: 2, Column: 16}, + Expression: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 19, Line: 2, Column: 18}, + EndPos: ast.Position{Offset: 19, Line: 2, Column: 18}, + }, + }, + TrailingSeparatorPos: ast.Position{Offset: 20, Line: 2, Column: 19}, + }, + { + Label: "y", + LabelStartPos: &ast.Position{Offset: 22, Line: 2, Column: 21}, + LabelEndPos: &ast.Position{Offset: 23, Line: 2, Column: 22}, + Expression: &ast.IntegerExpression{ + PositiveLiteral: []byte("2"), + Value: big.NewInt(2), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 25, Line: 2, Column: 24}, + EndPos: ast.Position{Offset: 25, Line: 2, Column: 24}, + }, + }, + TrailingSeparatorPos: ast.Position{Offset: 26, Line: 2, Column: 25}, + }, + { + Label: "z", + LabelStartPos: &ast.Position{Offset: 28, Line: 2, Column: 27}, + LabelEndPos: &ast.Position{Offset: 30, Line: 2, Column: 29}, + Expression: &ast.IntegerExpression{ + PositiveLiteral: []byte("3"), + Value: big.NewInt(3), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 32, Line: 2, Column: 31}, + EndPos: ast.Position{Offset: 32, Line: 2, Column: 31}, + }, + }, + TrailingSeparatorPos: ast.Position{Offset: 33, Line: 2, Column: 32}, + }, + }, + ArgumentsStartPos: ast.Position{Offset: 15, Line: 2, Column: 14}, + EndPos: ast.Position{Offset: 33, Line: 2, Column: 32}, + }, + StartPos: ast.Position{Offset: 6, Line: 2, Column: 5}, + }, + }, + result.Declarations(), + ) +} + +func TestParseOptionalMemberExpression(t *testing.T) { + + t.Parallel() + + const code = ` + let a = b?.c + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.VariableDeclaration{ + Access: ast.AccessNotSpecified, + IsConstant: true, + Identifier: ast.Identifier{ + Identifier: "a", + Pos: ast.Position{Offset: 10, Line: 2, Column: 9}, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationCopy, + Pos: ast.Position{Offset: 12, Line: 2, Column: 11}, + }, + Value: &ast.MemberExpression{ + Optional: true, + Expression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "b", + Pos: ast.Position{Offset: 14, Line: 2, Column: 13}, + }, + }, + Identifier: ast.Identifier{ + Identifier: "c", + Pos: ast.Position{Offset: 17, Line: 2, Column: 16}, + }, + AccessPos: ast.Position{Offset: 16, Line: 2, Column: 15}, + }, + StartPos: ast.Position{Offset: 6, Line: 2, Column: 5}, + }, + }, + result.Declarations(), + ) +} + +func TestParseIndexExpressionInVariableDeclaration(t *testing.T) { + + t.Parallel() + + const code = ` + let a = b[1] + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.VariableDeclaration{ + Access: ast.AccessNotSpecified, + IsConstant: true, + Identifier: ast.Identifier{ + Identifier: "a", + Pos: ast.Position{Offset: 10, Line: 2, Column: 9}, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationCopy, + Pos: ast.Position{Offset: 12, Line: 2, Column: 11}, + }, + Value: &ast.IndexExpression{ + TargetExpression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "b", + Pos: ast.Position{Offset: 14, Line: 2, Column: 13}, + }, + }, + IndexingExpression: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 16, Line: 2, Column: 15}, + EndPos: ast.Position{Offset: 16, Line: 2, Column: 15}, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 15, Line: 2, Column: 14}, + EndPos: ast.Position{Offset: 17, Line: 2, Column: 16}, + }, + }, + StartPos: ast.Position{Offset: 6, Line: 2, Column: 5}, + }, + }, + result.Declarations(), + ) +} + +func TestParseUnaryExpression(t *testing.T) { + + t.Parallel() + + const code = ` + let foo = -boo + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.VariableDeclaration{ + Access: ast.AccessNotSpecified, + IsConstant: true, + Identifier: ast.Identifier{ + Identifier: "foo", + Pos: ast.Position{Offset: 10, Line: 2, Column: 9}, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationCopy, + Pos: ast.Position{Offset: 14, Line: 2, Column: 13}, + }, + Value: &ast.UnaryExpression{ + Operation: ast.OperationMinus, + Expression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "boo", + Pos: ast.Position{Offset: 17, Line: 2, Column: 16}, + }, + }, + StartPos: ast.Position{Offset: 16, Line: 2, Column: 15}, + }, + StartPos: ast.Position{Offset: 6, Line: 2, Column: 5}, + }, + }, + result.Declarations(), + ) +} + +func TestParseOrExpression(t *testing.T) { + + t.Parallel() + + const code = ` + let a = false || true + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.VariableDeclaration{ + Access: ast.AccessNotSpecified, + IsConstant: true, + Identifier: ast.Identifier{ + Identifier: "a", + Pos: ast.Position{Offset: 13, Line: 2, Column: 12}, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationCopy, + Pos: ast.Position{Offset: 15, Line: 2, Column: 14}, + }, + Value: &ast.BinaryExpression{ + Operation: ast.OperationOr, + Left: &ast.BoolExpression{ + Value: false, + Range: ast.Range{ + StartPos: ast.Position{Offset: 17, Line: 2, Column: 16}, + EndPos: ast.Position{Offset: 21, Line: 2, Column: 20}, + }, + }, + Right: &ast.BoolExpression{ + Value: true, + Range: ast.Range{ + StartPos: ast.Position{Offset: 26, Line: 2, Column: 25}, + EndPos: ast.Position{Offset: 29, Line: 2, Column: 28}, + }, + }, + }, + StartPos: ast.Position{Offset: 9, Line: 2, Column: 8}, + }, + }, + result.Declarations(), + ) +} + +func TestParseAndExpression(t *testing.T) { + + t.Parallel() + + const code = ` + let a = false && true + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.VariableDeclaration{ + Access: ast.AccessNotSpecified, + IsConstant: true, + Identifier: ast.Identifier{ + Identifier: "a", + Pos: ast.Position{Offset: 13, Line: 2, Column: 12}, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationCopy, + Pos: ast.Position{Offset: 15, Line: 2, Column: 14}, + }, + Value: &ast.BinaryExpression{ + Operation: ast.OperationAnd, + Left: &ast.BoolExpression{ + Value: false, + Range: ast.Range{ + StartPos: ast.Position{Offset: 17, Line: 2, Column: 16}, + EndPos: ast.Position{Offset: 21, Line: 2, Column: 20}, + }, + }, + Right: &ast.BoolExpression{ + Value: true, + Range: ast.Range{ + StartPos: ast.Position{Offset: 26, Line: 2, Column: 25}, + EndPos: ast.Position{Offset: 29, Line: 2, Column: 28}, + }, + }, + }, + StartPos: ast.Position{Offset: 9, Line: 2, Column: 8}, + }, + }, + result.Declarations(), + ) +} + +func TestParseEqualityExpression(t *testing.T) { + + t.Parallel() + + const code = ` + let a = false == true + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.VariableDeclaration{ + Access: ast.AccessNotSpecified, + IsConstant: true, + Identifier: ast.Identifier{ + Identifier: "a", + Pos: ast.Position{Offset: 13, Line: 2, Column: 12}, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationCopy, + Pos: ast.Position{Offset: 15, Line: 2, Column: 14}, + }, + Value: &ast.BinaryExpression{ + Operation: ast.OperationEqual, + Left: &ast.BoolExpression{ + Value: false, + Range: ast.Range{ + StartPos: ast.Position{Offset: 17, Line: 2, Column: 16}, + EndPos: ast.Position{Offset: 21, Line: 2, Column: 20}, + }, + }, + Right: &ast.BoolExpression{ + Value: true, + Range: ast.Range{ + StartPos: ast.Position{Offset: 26, Line: 2, Column: 25}, + EndPos: ast.Position{Offset: 29, Line: 2, Column: 28}, + }, + }, + }, + StartPos: ast.Position{Offset: 9, Line: 2, Column: 8}, + }, + }, + result.Declarations(), + ) +} + +func TestParseRelationalExpression(t *testing.T) { + + t.Parallel() + + const code = ` + let a = 1 < 2 + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.VariableDeclaration{ + Access: ast.AccessNotSpecified, + IsConstant: true, + Identifier: ast.Identifier{ + Identifier: "a", + Pos: ast.Position{Offset: 13, Line: 2, Column: 12}, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationCopy, + Pos: ast.Position{Offset: 15, Line: 2, Column: 14}, + }, + Value: &ast.BinaryExpression{ + Operation: ast.OperationLess, + Left: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 17, Line: 2, Column: 16}, + EndPos: ast.Position{Offset: 17, Line: 2, Column: 16}, + }, + }, + Right: &ast.IntegerExpression{ + PositiveLiteral: []byte("2"), + Value: big.NewInt(2), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 21, Line: 2, Column: 20}, + EndPos: ast.Position{Offset: 21, Line: 2, Column: 20}, + }, + }, + }, + StartPos: ast.Position{Offset: 9, Line: 2, Column: 8}, + }, + }, + result.Declarations(), + ) +} + +func TestParseAdditiveExpression(t *testing.T) { + + t.Parallel() + + const code = ` + let a = 1 + 2 + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.VariableDeclaration{ + Access: ast.AccessNotSpecified, + IsConstant: true, + Identifier: ast.Identifier{ + Identifier: "a", + Pos: ast.Position{Offset: 13, Line: 2, Column: 12}, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationCopy, + Pos: ast.Position{Offset: 15, Line: 2, Column: 14}, + }, + Value: &ast.BinaryExpression{ + Operation: ast.OperationPlus, + Left: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 17, Line: 2, Column: 16}, + EndPos: ast.Position{Offset: 17, Line: 2, Column: 16}, + }, + }, + Right: &ast.IntegerExpression{ + PositiveLiteral: []byte("2"), + Value: big.NewInt(2), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 21, Line: 2, Column: 20}, + EndPos: ast.Position{Offset: 21, Line: 2, Column: 20}, + }, + }, + }, + StartPos: ast.Position{Offset: 9, Line: 2, Column: 8}, + }, + }, + result.Declarations(), + ) +} + +func TestParseMultiplicativeExpression(t *testing.T) { + + t.Parallel() + + const code = ` + let a = 1 * 2 + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.VariableDeclaration{ + Access: ast.AccessNotSpecified, + IsConstant: true, + Identifier: ast.Identifier{ + Identifier: "a", + Pos: ast.Position{Offset: 13, Line: 2, Column: 12}, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationCopy, + Pos: ast.Position{Offset: 15, Line: 2, Column: 14}, + }, + Value: &ast.BinaryExpression{ + Operation: ast.OperationMul, + Left: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 17, Line: 2, Column: 16}, + EndPos: ast.Position{Offset: 17, Line: 2, Column: 16}, + }, + }, + Right: &ast.IntegerExpression{ + PositiveLiteral: []byte("2"), + Value: big.NewInt(2), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 21, Line: 2, Column: 20}, + EndPos: ast.Position{Offset: 21, Line: 2, Column: 20}, + }, + }, + }, + StartPos: ast.Position{Offset: 9, Line: 2, Column: 8}, + }, + }, + result.Declarations(), + ) +} + +func TestParseFunctionExpressionAndReturn(t *testing.T) { + + t.Parallel() + + const code = ` + let test = fun (): Int { return 1 } + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.VariableDeclaration{ + Access: ast.AccessNotSpecified, + IsConstant: true, + Identifier: ast.Identifier{ + Identifier: "test", + Pos: ast.Position{Offset: 10, Line: 2, Column: 9}, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationCopy, + Pos: ast.Position{Offset: 15, Line: 2, Column: 14}, + }, + Value: &ast.FunctionExpression{ + ParameterList: &ast.ParameterList{ + Range: ast.Range{ + StartPos: ast.Position{Offset: 21, Line: 2, Column: 20}, + EndPos: ast.Position{Offset: 22, Line: 2, Column: 21}, + }, + }, + ReturnTypeAnnotation: &ast.TypeAnnotation{ + IsResource: false, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Int", + Pos: ast.Position{Offset: 25, Line: 2, Column: 24}, + }, + }, + StartPos: ast.Position{Offset: 25, Line: 2, Column: 24}, + }, + FunctionBlock: &ast.FunctionBlock{ + Block: &ast.Block{ + Statements: []ast.Statement{ + &ast.ReturnStatement{ + Expression: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 38, Line: 2, Column: 37}, + EndPos: ast.Position{Offset: 38, Line: 2, Column: 37}, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 31, Line: 2, Column: 30}, + EndPos: ast.Position{Offset: 38, Line: 2, Column: 37}, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 29, Line: 2, Column: 28}, + EndPos: ast.Position{Offset: 40, Line: 2, Column: 39}, + }, + }, + }, + StartPos: ast.Position{Offset: 17, Line: 2, Column: 16}, + }, + StartPos: ast.Position{Offset: 6, Line: 2, Column: 5}, + }, + }, + result.Declarations(), + ) +} + +func TestParseLeftAssociativity(t *testing.T) { + + t.Parallel() + + const code = ` + let a = 1 + 2 + 3 + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.VariableDeclaration{ + Access: ast.AccessNotSpecified, + IsConstant: true, + Identifier: ast.Identifier{ + Identifier: "a", + Pos: ast.Position{Offset: 13, Line: 2, Column: 12}, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationCopy, + Pos: ast.Position{Offset: 15, Line: 2, Column: 14}, + }, + Value: &ast.BinaryExpression{ + Operation: ast.OperationPlus, + Left: &ast.BinaryExpression{ + Operation: ast.OperationPlus, + Left: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 17, Line: 2, Column: 16}, + EndPos: ast.Position{Offset: 17, Line: 2, Column: 16}, + }, + }, + Right: &ast.IntegerExpression{ + PositiveLiteral: []byte("2"), + Value: big.NewInt(2), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 21, Line: 2, Column: 20}, + EndPos: ast.Position{Offset: 21, Line: 2, Column: 20}, + }, + }, + }, + Right: &ast.IntegerExpression{ + PositiveLiteral: []byte("3"), + Value: big.NewInt(3), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 25, Line: 2, Column: 24}, + EndPos: ast.Position{Offset: 25, Line: 2, Column: 24}, + }, + }, + }, + StartPos: ast.Position{Offset: 9, Line: 2, Column: 8}, + }, + }, + result.Declarations(), + ) +} + +func TestParseNegativeInteger(t *testing.T) { + + t.Parallel() + + const code = ` + let a = -42 + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.VariableDeclaration{ + Access: ast.AccessNotSpecified, + IsConstant: true, + Identifier: ast.Identifier{ + Identifier: "a", + Pos: ast.Position{Offset: 11, Line: 2, Column: 10}, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationCopy, + Pos: ast.Position{Offset: 13, Line: 2, Column: 12}, + }, + Value: &ast.IntegerExpression{ + PositiveLiteral: []byte("42"), + Value: big.NewInt(-42), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 15, Line: 2, Column: 14}, + EndPos: ast.Position{Offset: 17, Line: 2, Column: 16}, + }, + }, + StartPos: ast.Position{Offset: 7, Line: 2, Column: 6}, + }, + }, + result.Declarations(), + ) +} + +func TestParseNegativeFixedPoint(t *testing.T) { + + t.Parallel() + + const code = ` + let a = -42.3 + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.VariableDeclaration{ + Access: ast.AccessNotSpecified, + IsConstant: true, + Identifier: ast.Identifier{ + Identifier: "a", + Pos: ast.Position{Offset: 11, Line: 2, Column: 10}, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationCopy, + Pos: ast.Position{Offset: 13, Line: 2, Column: 12}, + }, + Value: &ast.FixedPointExpression{ + PositiveLiteral: []byte("42.3"), + Negative: true, + UnsignedInteger: big.NewInt(42), + Fractional: big.NewInt(3), + Scale: 1, + Range: ast.Range{ + StartPos: ast.Position{Offset: 15, Line: 2, Column: 14}, + EndPos: ast.Position{Offset: 19, Line: 2, Column: 18}, + }, + }, + StartPos: ast.Position{Offset: 7, Line: 2, Column: 6}, + }, + }, + result.Declarations(), + ) +} + +func TestParseTernaryRightAssociativity(t *testing.T) { + + t.Parallel() + + const code = ` + let a = 2 > 1 + ? 0 + : 3 > 2 ? 1 : 2 + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.VariableDeclaration{ + Access: ast.AccessNotSpecified, + IsConstant: true, + Identifier: ast.Identifier{ + Identifier: "a", + Pos: ast.Position{Offset: 13, Line: 2, Column: 12}, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationCopy, + Pos: ast.Position{Offset: 15, Line: 2, Column: 14}, + }, + Value: &ast.ConditionalExpression{ + Test: &ast.BinaryExpression{ + Operation: ast.OperationGreater, + Left: &ast.IntegerExpression{ + PositiveLiteral: []byte("2"), + Value: big.NewInt(2), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 17, Line: 2, Column: 16}, + EndPos: ast.Position{Offset: 17, Line: 2, Column: 16}, + }, + }, + Right: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 21, Line: 2, Column: 20}, + EndPos: ast.Position{Offset: 21, Line: 2, Column: 20}, + }, + }, + }, + Then: &ast.IntegerExpression{ + PositiveLiteral: []byte("0"), + Value: new(big.Int), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 35, Line: 3, Column: 12}, + EndPos: ast.Position{Offset: 35, Line: 3, Column: 12}, + }, + }, + Else: &ast.ConditionalExpression{ + Test: &ast.BinaryExpression{ + Operation: ast.OperationGreater, + Left: &ast.IntegerExpression{ + PositiveLiteral: []byte("3"), + Value: big.NewInt(3), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 49, Line: 4, Column: 12}, + EndPos: ast.Position{Offset: 49, Line: 4, Column: 12}, + }, + }, + Right: &ast.IntegerExpression{ + PositiveLiteral: []byte("2"), + Value: big.NewInt(2), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 53, Line: 4, Column: 16}, + EndPos: ast.Position{Offset: 53, Line: 4, Column: 16}, + }, + }, + }, + Then: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 57, Line: 4, Column: 20}, + EndPos: ast.Position{Offset: 57, Line: 4, Column: 20}, + }, + }, + Else: &ast.IntegerExpression{ + PositiveLiteral: []byte("2"), + Value: big.NewInt(2), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 61, Line: 4, Column: 24}, + EndPos: ast.Position{Offset: 61, Line: 4, Column: 24}, + }, + }, + }, + }, + StartPos: ast.Position{Offset: 9, Line: 2, Column: 8}, + }, + }, + result.Declarations(), + ) +} + +func TestParseVoidLiteral(t *testing.T) { + t.Parallel() + + const code = ` + let void: Void = () + ` + + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.VariableDeclaration{ + Access: ast.AccessNotSpecified, + IsConstant: true, + Identifier: ast.Identifier{ + Identifier: "void", + Pos: ast.Position{Offset: 7, Line: 2, Column: 6}, + }, + TypeAnnotation: &ast.TypeAnnotation{ + IsResource: false, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Void", + Pos: ast.Position{Offset: 13, Line: 2, Column: 12}, + }, + NestedIdentifiers: nil, + }, + StartPos: ast.Position{Offset: 13, Line: 2, Column: 12}, + }, + Value: &ast.VoidExpression{ + Range: ast.Range{ + StartPos: ast.Position{Offset: 20, Line: 2, Column: 19}, + EndPos: ast.Position{Offset: 23, Line: 3, Column: 0}, + }, + }, + Transfer: &ast.Transfer{ + Operation: 1, + Pos: ast.Position{Offset: 18, Line: 2, Column: 17}, + }, + StartPos: ast.Position{Offset: 3, Line: 2, Column: 2}, + }, + }, result.Declarations()) +} + +func TestParseMissingReturnType(t *testing.T) { + + t.Parallel() + + const code = ` + let noop: ((): Void) = + fun () { return } + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.VariableDeclaration{ + Access: ast.AccessNotSpecified, + Identifier: ast.Identifier{ + Identifier: "noop", + Pos: ast.Position{Offset: 7, Line: 2, Column: 6}, + }, + + IsConstant: true, + TypeAnnotation: &ast.TypeAnnotation{ + IsResource: false, + Type: &ast.FunctionType{ + ReturnTypeAnnotation: &ast.TypeAnnotation{ + IsResource: false, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Void", + Pos: ast.Position{Offset: 18, Line: 2, Column: 17}, + }, + }, + StartPos: ast.Position{Offset: 18, Line: 2, Column: 17}, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 13, Line: 2, Column: 12}, + EndPos: ast.Position{Offset: 22, Line: 2, Column: 21}, + }, + }, + StartPos: ast.Position{Offset: 13, Line: 2, Column: 12}, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationCopy, + Pos: ast.Position{Offset: 24, Line: 2, Column: 23}, + }, + Value: &ast.FunctionExpression{ + ParameterList: &ast.ParameterList{ + Range: ast.Range{ + StartPos: ast.Position{Offset: 42, Line: 3, Column: 16}, + EndPos: ast.Position{Offset: 43, Line: 3, Column: 17}, + }, + }, + FunctionBlock: &ast.FunctionBlock{ + Block: &ast.Block{ + Statements: []ast.Statement{ + &ast.ReturnStatement{ + Range: ast.Range{ + StartPos: ast.Position{Offset: 47, Line: 3, Column: 21}, + EndPos: ast.Position{Offset: 52, Line: 3, Column: 26}, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 45, Line: 3, Column: 19}, + EndPos: ast.Position{Offset: 54, Line: 3, Column: 28}, + }, + }, + }, + StartPos: ast.Position{Offset: 38, Line: 3, Column: 12}, + }, + StartPos: ast.Position{Offset: 3, Line: 2, Column: 2}, + }, + }, + result.Declarations(), + ) +} + +func TestParseExpression(t *testing.T) { + + t.Parallel() + + actual, errs := testParseExpression(` + before(x + before(y)) + z + `) + var err error + if len(errs) > 0 { + err = Error{ + Errors: errs, + } + } + require.NoError(t, err) + + expected := &ast.BinaryExpression{ + Operation: ast.OperationPlus, + Left: &ast.InvocationExpression{ + InvokedExpression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "before", + Pos: ast.Position{Offset: 9, Line: 2, Column: 8}, + }, + }, + Arguments: []*ast.Argument{ + { + Label: "", + LabelStartPos: nil, + LabelEndPos: nil, + Expression: &ast.BinaryExpression{ + Operation: ast.OperationPlus, + Left: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "x", + Pos: ast.Position{Offset: 16, Line: 2, Column: 15}, + }, + }, + Right: &ast.InvocationExpression{ + InvokedExpression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "before", + Pos: ast.Position{Offset: 20, Line: 2, Column: 19}, + }, + }, + Arguments: []*ast.Argument{ + { + Label: "", + LabelStartPos: nil, + LabelEndPos: nil, + Expression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "y", + Pos: ast.Position{Offset: 27, Line: 2, Column: 26}, + }, + }, + TrailingSeparatorPos: ast.Position{Offset: 28, Line: 2, Column: 27}, + }, + }, + ArgumentsStartPos: ast.Position{Offset: 26, Line: 2, Column: 25}, + EndPos: ast.Position{Offset: 28, Line: 2, Column: 27}, + }, + }, + TrailingSeparatorPos: ast.Position{Offset: 29, Line: 2, Column: 28}, + }, + }, + ArgumentsStartPos: ast.Position{Offset: 15, Line: 2, Column: 14}, + EndPos: ast.Position{Offset: 29, Line: 2, Column: 28}, + }, + Right: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "z", + Pos: ast.Position{Offset: 33, Line: 2, Column: 32}, + }, + }, + } + + utils.AssertEqualWithDiff(t, expected, actual) +} + +func TestParseStringEscapes(t *testing.T) { + + t.Parallel() + + actual, errs := testParseExpression(` + "test \0\n\r\t\"\'\\ xyz" + `) + + var err error + if len(errs) > 0 { + err = Error{ + Errors: errs, + } + } + + require.NoError(t, err) + + expected := &ast.StringExpression{ + Value: "test \x00\n\r\t\"'\\ xyz", + Range: ast.Range{ + StartPos: ast.Position{Offset: 8, Line: 2, Column: 7}, + EndPos: ast.Position{Offset: 32, Line: 2, Column: 31}, + }, + } + + utils.AssertEqualWithDiff(t, expected, actual) +} + +func TestParseStringWithUnicode(t *testing.T) { + + t.Parallel() + + actual, errs := testParseExpression(` + "this is a test \t\\new line and race car:\n\u{1F3CE}\u{FE0F}" + `) + + var err error + if len(errs) > 0 { + err = Error{ + Errors: errs, + } + } + + require.NoError(t, err) + + expected := &ast.StringExpression{ + Value: "this is a test \t\\new line and race car:\n\U0001F3CE\uFE0F", + Range: ast.Range{ + StartPos: ast.Position{Offset: 7, Line: 2, Column: 6}, + EndPos: ast.Position{Offset: 68, Line: 2, Column: 67}, + }, + } + + utils.AssertEqualWithDiff(t, expected, actual) +} + +func TestParseNilCoalescing(t *testing.T) { + + t.Parallel() + + const code = ` + let x = nil ?? 1 + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.VariableDeclaration{ + Access: ast.AccessNotSpecified, + IsConstant: true, + Identifier: ast.Identifier{ + Identifier: "x", + Pos: ast.Position{Offset: 12, Line: 2, Column: 11}, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationCopy, + Pos: ast.Position{Offset: 14, Line: 2, Column: 13}, + }, + Value: &ast.BinaryExpression{ + Operation: ast.OperationNilCoalesce, + Left: &ast.NilExpression{ + Pos: ast.Position{Offset: 16, Line: 2, Column: 15}, + }, + Right: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 23, Line: 2, Column: 22}, + EndPos: ast.Position{Offset: 23, Line: 2, Column: 22}, + }, + }, + }, + StartPos: ast.Position{Offset: 8, Line: 2, Column: 7}, + }, + }, + result.Declarations(), + ) +} + +func TestParseNilCoalescingRightAssociativity(t *testing.T) { + + t.Parallel() + + // NOTE: only syntactically, not semantically valid + const code = ` + let x = 1 ?? 2 ?? 3 + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.VariableDeclaration{ + Access: ast.AccessNotSpecified, + IsConstant: true, + Identifier: ast.Identifier{ + Identifier: "x", + Pos: ast.Position{Offset: 12, Line: 2, Column: 11}, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationCopy, + Pos: ast.Position{Offset: 14, Line: 2, Column: 13}, + }, + Value: &ast.BinaryExpression{ + Operation: ast.OperationNilCoalesce, + Left: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 16, Line: 2, Column: 15}, + EndPos: ast.Position{Offset: 16, Line: 2, Column: 15}, + }, + }, + Right: &ast.BinaryExpression{ + Operation: ast.OperationNilCoalesce, + Left: &ast.IntegerExpression{ + PositiveLiteral: []byte("2"), + Value: big.NewInt(2), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 21, Line: 2, Column: 20}, + EndPos: ast.Position{Offset: 21, Line: 2, Column: 20}, + }, + }, + Right: &ast.IntegerExpression{ + PositiveLiteral: []byte("3"), + Value: big.NewInt(3), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 26, Line: 2, Column: 25}, + EndPos: ast.Position{Offset: 26, Line: 2, Column: 25}, + }, + }, + }, + }, + StartPos: ast.Position{Offset: 8, Line: 2, Column: 7}, + }, + }, + result.Declarations(), + ) +} + +func TestParseFailableCasting(t *testing.T) { + + t.Parallel() + + const code = ` + let x = 0 as? Int + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + failableDowncast := &ast.CastingExpression{ + Expression: &ast.IntegerExpression{ + PositiveLiteral: []byte("0"), + Value: new(big.Int), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 16, Line: 2, Column: 15}, + EndPos: ast.Position{Offset: 16, Line: 2, Column: 15}, + }, + }, + Operation: ast.OperationFailableCast, + TypeAnnotation: &ast.TypeAnnotation{ + IsResource: false, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Int", + Pos: ast.Position{Offset: 22, Line: 2, Column: 21}, + }, + }, + StartPos: ast.Position{Offset: 22, Line: 2, Column: 21}, + }, + } + + variableDeclaration := &ast.VariableDeclaration{ + Access: ast.AccessNotSpecified, + IsConstant: true, + Identifier: ast.Identifier{ + Identifier: "x", + Pos: ast.Position{Offset: 12, Line: 2, Column: 11}, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationCopy, + Pos: ast.Position{Offset: 14, Line: 2, Column: 13}, + }, + Value: failableDowncast, + StartPos: ast.Position{Offset: 8, Line: 2, Column: 7}, + } + + failableDowncast.ParentVariableDeclaration = variableDeclaration + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + variableDeclaration, + }, + result.Declarations(), + ) +} + +func TestParseMoveOperator(t *testing.T) { + + t.Parallel() + + const code = ` + let x = foo(<-y) + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.VariableDeclaration{ + Access: ast.AccessNotSpecified, + IsConstant: true, + Identifier: ast.Identifier{ + Identifier: "x", + Pos: ast.Position{Offset: 11, Line: 2, Column: 10}, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationCopy, + Pos: ast.Position{Offset: 13, Line: 2, Column: 12}, + }, + Value: &ast.InvocationExpression{ + InvokedExpression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "foo", + Pos: ast.Position{Offset: 15, Line: 2, Column: 14}, + }, + }, + Arguments: []*ast.Argument{ + { + Label: "", + LabelStartPos: nil, + LabelEndPos: nil, + Expression: &ast.UnaryExpression{ + Operation: ast.OperationMove, + Expression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "y", + Pos: ast.Position{Offset: 21, Line: 2, Column: 20}, + }, + }, + StartPos: ast.Position{Offset: 19, Line: 2, Column: 18}, + }, + TrailingSeparatorPos: ast.Position{Offset: 22, Line: 2, Column: 21}, + }, + }, + ArgumentsStartPos: ast.Position{Offset: 18, Line: 2, Column: 17}, + EndPos: ast.Position{Offset: 22, Line: 2, Column: 21}, + }, + StartPos: ast.Position{Offset: 7, Line: 2, Column: 6}, + }, + }, + result.Declarations(), + ) +} + +func TestParseFunctionExpressionWithResourceTypeAnnotation(t *testing.T) { + + t.Parallel() + + const code = ` + let f = fun (): @R { return X } + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + + &ast.VariableDeclaration{ + Access: ast.AccessNotSpecified, + IsConstant: true, + Identifier: ast.Identifier{ + Identifier: "f", + Pos: ast.Position{Offset: 13, Line: 2, Column: 12}, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationCopy, + Pos: ast.Position{Offset: 15, Line: 2, Column: 14}, + }, + Value: &ast.FunctionExpression{ + ParameterList: &ast.ParameterList{ + Range: ast.Range{ + StartPos: ast.Position{Offset: 21, Line: 2, Column: 20}, + EndPos: ast.Position{Offset: 22, Line: 2, Column: 21}, + }, + }, + ReturnTypeAnnotation: &ast.TypeAnnotation{ + IsResource: true, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "R", + Pos: ast.Position{Offset: 26, Line: 2, Column: 25}, + }, + }, + StartPos: ast.Position{Offset: 25, Line: 2, Column: 24}, + }, + FunctionBlock: &ast.FunctionBlock{ + Block: &ast.Block{ + Statements: []ast.Statement{ + &ast.ReturnStatement{ + Expression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "X", + Pos: ast.Position{Offset: 37, Line: 2, Column: 36}, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 30, Line: 2, Column: 29}, + EndPos: ast.Position{Offset: 37, Line: 2, Column: 36}, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 28, Line: 2, Column: 27}, + EndPos: ast.Position{Offset: 39, Line: 2, Column: 38}, + }, + }, + }, + StartPos: ast.Position{Offset: 17, Line: 2, Column: 16}, + }, + StartPos: ast.Position{Offset: 9, Line: 2, Column: 8}, + }, + }, + result.Declarations(), + ) +} + +func TestParseFailableCastingResourceTypeAnnotation(t *testing.T) { + + t.Parallel() + + const code = ` + let y = x as? @R + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + failableDowncast := &ast.CastingExpression{ + Expression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "x", + Pos: ast.Position{Offset: 17, Line: 2, Column: 16}, + }, + }, + Operation: ast.OperationFailableCast, + TypeAnnotation: &ast.TypeAnnotation{ + IsResource: true, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "R", + Pos: ast.Position{Offset: 24, Line: 2, Column: 23}, + }, + }, + StartPos: ast.Position{Offset: 23, Line: 2, Column: 22}, + }, + } + + variableDeclaration := &ast.VariableDeclaration{ + Access: ast.AccessNotSpecified, + IsConstant: true, + Identifier: ast.Identifier{ + Identifier: "y", + Pos: ast.Position{Offset: 13, Line: 2, Column: 12}, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationCopy, + Pos: ast.Position{Offset: 15, Line: 2, Column: 14}, + }, + Value: failableDowncast, + StartPos: ast.Position{Offset: 9, Line: 2, Column: 8}, + } + + failableDowncast.ParentVariableDeclaration = variableDeclaration + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + variableDeclaration, + }, + result.Declarations(), + ) +} + +func TestParseCasting(t *testing.T) { + + t.Parallel() + + const code = ` + let y = x as Y + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + cast := &ast.CastingExpression{ + Expression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "x", + Pos: ast.Position{Offset: 17, Line: 2, Column: 16}, + }, + }, + Operation: ast.OperationCast, + TypeAnnotation: &ast.TypeAnnotation{ + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Y", + Pos: ast.Position{Offset: 22, Line: 2, Column: 21}, + }, + }, + StartPos: ast.Position{Offset: 22, Line: 2, Column: 21}, + }, + } + + variableDeclaration := &ast.VariableDeclaration{ + Access: ast.AccessNotSpecified, + IsConstant: true, + Identifier: ast.Identifier{ + Identifier: "y", + Pos: ast.Position{Offset: 13, Line: 2, Column: 12}, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationCopy, + Pos: ast.Position{Offset: 15, Line: 2, Column: 14}, + }, + Value: cast, + StartPos: ast.Position{Offset: 9, Line: 2, Column: 8}, + } + + cast.ParentVariableDeclaration = variableDeclaration + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + variableDeclaration, + }, + result.Declarations(), + ) +} + +func TestParseIdentifiers(t *testing.T) { + + t.Parallel() + + for _, name := range []string{"foo", "from", "create", "destroy", "for", "in"} { + t.Run(name, func(t *testing.T) { + code := fmt.Sprintf(`let %s = 1`, name) + _, errs := testParseProgram(code) + require.Empty(t, errs) + }) + } +} + +func TestParseReferenceInVariableDeclaration(t *testing.T) { + + t.Parallel() + + const code = ` + let x = &account.storage[R] as &R + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.VariableDeclaration{ + Access: ast.AccessNotSpecified, + IsConstant: true, + Identifier: ast.Identifier{ + Identifier: "x", + Pos: ast.Position{Offset: 12, Line: 2, Column: 11}, + }, + Value: &ast.ReferenceExpression{ + Expression: &ast.IndexExpression{ + TargetExpression: &ast.MemberExpression{ + Expression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "account", + Pos: ast.Position{Offset: 17, Line: 2, Column: 16}, + }, + }, + AccessPos: ast.Position{Offset: 24, Line: 2, Column: 23}, + Identifier: ast.Identifier{ + Identifier: "storage", + Pos: ast.Position{Offset: 25, Line: 2, Column: 24}, + }, + }, + IndexingExpression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "R", + Pos: ast.Position{Offset: 33, Line: 2, Column: 32}, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 32, Line: 2, Column: 31}, + EndPos: ast.Position{Offset: 34, Line: 2, Column: 33}, + }, + }, + StartPos: ast.Position{Offset: 16, Line: 2, Column: 15}, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationCopy, + Pos: ast.Position{Offset: 14, Line: 2, Column: 13}, + }, + StartPos: ast.Position{Offset: 8, Line: 2, Column: 7}, + }, + }, + result.Declarations(), + ) +} + +func TestParseFixedPointExpression(t *testing.T) { + + t.Parallel() + + const code = ` + let a = -1234_5678_90.0009_8765_4321 + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.VariableDeclaration{ + Access: ast.AccessNotSpecified, + IsConstant: true, + Identifier: ast.Identifier{Identifier: "a", + Pos: ast.Position{Offset: 10, Line: 2, Column: 9}, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationCopy, + Pos: ast.Position{Offset: 12, Line: 2, Column: 11}, + }, + Value: &ast.FixedPointExpression{ + PositiveLiteral: []byte("1234_5678_90.0009_8765_4321"), + Negative: true, + UnsignedInteger: big.NewInt(1234567890), + Fractional: big.NewInt(987654321), + Scale: 12, + Range: ast.Range{ + StartPos: ast.Position{Offset: 14, Line: 2, Column: 13}, + EndPos: ast.Position{Offset: 41, Line: 2, Column: 40}, + }, + }, + StartPos: ast.Position{Offset: 6, Line: 2, Column: 5}, + }, + }, + result.Declarations(), + ) +} + +func TestParseFixedPointExpressionZeroInteger(t *testing.T) { + + t.Parallel() + + const code = ` + let a = -0.1 + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.VariableDeclaration{ + Access: ast.AccessNotSpecified, + IsConstant: true, + Identifier: ast.Identifier{Identifier: "a", + Pos: ast.Position{Offset: 10, Line: 2, Column: 9}, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationCopy, + Pos: ast.Position{Offset: 12, Line: 2, Column: 11}, + }, + Value: &ast.FixedPointExpression{ + PositiveLiteral: []byte("0.1"), + Negative: true, + UnsignedInteger: new(big.Int), + Fractional: big.NewInt(1), + Scale: 1, + Range: ast.Range{ + StartPos: ast.Position{Offset: 14, Line: 2, Column: 13}, + EndPos: ast.Position{Offset: 17, Line: 2, Column: 16}, + }, + }, + StartPos: ast.Position{Offset: 6, Line: 2, Column: 5}, + }, + }, + result.Declarations(), + ) +} + +func TestParsePathLiteral(t *testing.T) { + + t.Parallel() + + const code = ` + let a = /foo/bar + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.VariableDeclaration{ + Access: ast.AccessNotSpecified, + IsConstant: true, + Identifier: ast.Identifier{Identifier: "a", + Pos: ast.Position{Offset: 10, Line: 2, Column: 9}, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationCopy, + Pos: ast.Position{Offset: 12, Line: 2, Column: 11}, + }, + Value: &ast.PathExpression{ + StartPos: ast.Position{Offset: 14, Line: 2, Column: 13}, + Domain: ast.Identifier{ + Identifier: "foo", + Pos: ast.Position{Offset: 15, Line: 2, Column: 14}, + }, + Identifier: ast.Identifier{ + Identifier: "bar", + Pos: ast.Position{Offset: 19, Line: 2, Column: 18}, + }, + }, + StartPos: ast.Position{Offset: 6, Line: 2, Column: 5}, + }, + }, + result.Declarations(), + ) +} + +func TestParseBitwiseExpression(t *testing.T) { + + t.Parallel() + + const code = ` + let a = 1 | 2 ^ 3 & 4 << 5 >> 6 + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.VariableDeclaration{ + Access: ast.AccessNotSpecified, + IsConstant: true, + Identifier: ast.Identifier{ + Identifier: "a", + Pos: ast.Position{Offset: 11, Line: 2, Column: 10}, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationCopy, + Pos: ast.Position{Offset: 13, Line: 2, Column: 12}, + }, + Value: &ast.BinaryExpression{ + Operation: ast.OperationBitwiseOr, + Left: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 15, Line: 2, Column: 14}, + EndPos: ast.Position{Offset: 15, Line: 2, Column: 14}, + }, + }, + Right: &ast.BinaryExpression{ + Operation: ast.OperationBitwiseXor, + Left: &ast.IntegerExpression{ + PositiveLiteral: []byte("2"), + Value: big.NewInt(2), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 19, Line: 2, Column: 18}, + EndPos: ast.Position{Offset: 19, Line: 2, Column: 18}, + }, + }, + Right: &ast.BinaryExpression{ + Operation: ast.OperationBitwiseAnd, + Left: &ast.IntegerExpression{ + PositiveLiteral: []byte("3"), + Value: big.NewInt(3), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 23, Line: 2, Column: 22}, + EndPos: ast.Position{Offset: 23, Line: 2, Column: 22}, + }, + }, + Right: &ast.BinaryExpression{ + Operation: ast.OperationBitwiseRightShift, + Left: &ast.BinaryExpression{ + Operation: ast.OperationBitwiseLeftShift, + Left: &ast.IntegerExpression{ + PositiveLiteral: []byte("4"), + Value: big.NewInt(4), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 27, Line: 2, Column: 26}, + EndPos: ast.Position{Offset: 27, Line: 2, Column: 26}, + }, + }, + Right: &ast.IntegerExpression{ + PositiveLiteral: []byte("5"), + Value: big.NewInt(5), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 32, Line: 2, Column: 31}, + EndPos: ast.Position{Offset: 32, Line: 2, Column: 31}, + }, + }, + }, + Right: &ast.IntegerExpression{ + PositiveLiteral: []byte("6"), + Value: big.NewInt(6), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 37, Line: 2, Column: 36}, + EndPos: ast.Position{Offset: 37, Line: 2, Column: 36}, + }, + }, + }, + }, + }, + }, + StartPos: ast.Position{Offset: 7, Line: 2, Column: 6}, + }, + }, + result.Declarations(), + ) +} + +func TestParseInvalidNegativeIntegerLiteralWithIncorrectPrefix(t *testing.T) { + + t.Parallel() + + const code = ` + let e = -0K0 + ` + _, err := testParseProgram(code) + + require.Error(t, err) +} + +type limitingMemoryGauge struct { + limited map[common.MemoryKind]bool // which kinds to limit + limits map[common.MemoryKind]uint64 // limits of limited kinds + totals map[common.MemoryKind]uint64 // metered memory. for debugging + debug bool // print totals after each allocation +} + +type limitingMemoryGaugeError string + +func (e limitingMemoryGaugeError) Error() string { + return string(e) +} + +func makeLimitingMemoryGauge() *limitingMemoryGauge { + g := limitingMemoryGauge{ + limited: make(map[common.MemoryKind]bool), + limits: make(map[common.MemoryKind]uint64), + totals: make(map[common.MemoryKind]uint64), + } + return &g +} + +func (g *limitingMemoryGauge) Limit(kind common.MemoryKind, limit uint64) { + g.limited[kind] = true + g.limits[kind] = limit +} + +func (g *limitingMemoryGauge) MeterMemory(usage common.MemoryUsage) error { + g.totals[usage.Kind] += usage.Amount + + if g.debug { + fmt.Println(g.totals) + } + + if !g.limited[usage.Kind] { + return nil + } + + if g.limits[usage.Kind] < usage.Amount { + return limitingMemoryGaugeError(fmt.Sprintf(`reached limit for "%s"`, usage.Kind.String())) + } + + g.limits[usage.Kind] -= usage.Amount + + return nil +} diff --git a/runtime/old_parser/function.go b/runtime/old_parser/function.go new file mode 100644 index 0000000000..ae3bb1ee99 --- /dev/null +++ b/runtime/old_parser/function.go @@ -0,0 +1,401 @@ +/* + * Cadence - The resource-oriented smart contract programming language + * + * Copyright Dapper Labs, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package old_parser + +import ( + "github.com/onflow/cadence/runtime/ast" + "github.com/onflow/cadence/runtime/parser/lexer" +) + +func parseParameterList(p *parser) (*ast.ParameterList, error) { + var parameters []*ast.Parameter + + p.skipSpaceAndComments() + + if !p.current.Is(lexer.TokenParenOpen) { + return nil, p.syntaxError( + "expected %s as start of parameter list, got %s", + lexer.TokenParenOpen, + p.current.Type, + ) + } + + startPos := p.current.StartPos + // Skip the opening paren + p.next() + + var endPos ast.Position + + expectParameter := true + + atEnd := false + for !atEnd { + p.skipSpaceAndComments() + switch p.current.Type { + case lexer.TokenIdentifier: + if !expectParameter { + p.report(&MissingCommaInParameterListError{ + Pos: p.current.StartPos, + }) + } + parameter, err := parseParameter(p) + if err != nil { + return nil, err + } + + parameters = append(parameters, parameter) + expectParameter = false + + case lexer.TokenComma: + if expectParameter { + return nil, p.syntaxError( + "expected parameter or end of parameter list, got %s", + p.current.Type, + ) + } + // Skip the comma + p.next() + expectParameter = true + + case lexer.TokenParenClose: + endPos = p.current.EndPos + // Skip the closing paren + p.next() + atEnd = true + + case lexer.TokenEOF: + return nil, p.syntaxError( + "missing %s at end of parameter list", + lexer.TokenParenClose, + ) + + default: + if expectParameter { + return nil, p.syntaxError( + "expected parameter or end of parameter list, got %s", + p.current.Type, + ) + } else { + return nil, p.syntaxError( + "expected comma or end of parameter list, got %s", + p.current.Type, + ) + } + } + } + + return ast.NewParameterList( + p.memoryGauge, + parameters, + ast.NewRange( + p.memoryGauge, + startPos, + endPos, + ), + ), nil +} + +func parseParameter(p *parser) (*ast.Parameter, error) { + p.skipSpaceAndComments() + + startPos := p.current.StartPos + parameterPos := startPos + + if !p.current.Is(lexer.TokenIdentifier) { + return nil, p.syntaxError( + "expected argument label or parameter name, got %s", + p.current.Type, + ) + } + + var argumentLabel string + parameterName := string(p.currentTokenSource()) + + // Skip the identifier + p.nextSemanticToken() + + // If another identifier is provided, then the previous identifier + // is the argument label, and this identifier is the parameter name + if p.current.Is(lexer.TokenIdentifier) { + argumentLabel = parameterName + parameterName = string(p.currentTokenSource()) + parameterPos = p.current.StartPos + // Skip the identifier + p.nextSemanticToken() + } + + if !p.current.Is(lexer.TokenColon) { + return nil, p.syntaxError( + "expected %s after argument label/parameter name, got %s", + lexer.TokenColon, + p.current.Type, + ) + } + + // Skip the colon + p.nextSemanticToken() + + typeAnnotation, err := parseTypeAnnotation(p) + if err != nil { + return nil, err + } + + return ast.NewParameter( + p.memoryGauge, + argumentLabel, + ast.NewIdentifier( + p.memoryGauge, + parameterName, + parameterPos, + ), + typeAnnotation, + nil, + startPos, + ), nil +} + +func parseTypeParameterList(p *parser) (*ast.TypeParameterList, error) { + var typeParameters []*ast.TypeParameter + + p.skipSpaceAndComments() + + if !p.current.Is(lexer.TokenLess) { + return nil, nil + } + + startPos := p.current.StartPos + // Skip the opening paren + p.next() + + var endPos ast.Position + + expectTypeParameter := true + + atEnd := false + for !atEnd { + p.skipSpaceAndComments() + switch p.current.Type { + case lexer.TokenIdentifier: + if !expectTypeParameter { + p.report(&MissingCommaInParameterListError{ + Pos: p.current.StartPos, + }) + } + typeParameter, err := parseTypeParameter(p) + if err != nil { + return nil, err + } + + typeParameters = append(typeParameters, typeParameter) + expectTypeParameter = false + + case lexer.TokenComma: + if expectTypeParameter { + return nil, p.syntaxError( + "expected type parameter or end of type parameter list, got %s", + p.current.Type, + ) + } + // Skip the comma + p.next() + expectTypeParameter = true + + case lexer.TokenGreater: + endPos = p.current.EndPos + // Skip the closing paren + p.next() + atEnd = true + + case lexer.TokenEOF: + return nil, p.syntaxError( + "missing %s at end of type parameter list", + lexer.TokenGreater, + ) + + default: + if expectTypeParameter { + return nil, p.syntaxError( + "expected parameter or end of type parameter list, got %s", + p.current.Type, + ) + } else { + return nil, p.syntaxError( + "expected comma or end of type parameter list, got %s", + p.current.Type, + ) + } + } + } + + return ast.NewTypeParameterList( + p.memoryGauge, + typeParameters, + ast.NewRange( + p.memoryGauge, + startPos, + endPos, + ), + ), nil +} + +func parseTypeParameter(p *parser) (*ast.TypeParameter, error) { + p.skipSpaceAndComments() + + if !p.current.Is(lexer.TokenIdentifier) { + return nil, p.syntaxError( + "expected type parameter name, got %s", + p.current.Type, + ) + } + + identifier := p.tokenToIdentifier(p.current) + p.nextSemanticToken() + + var err error + var typeBound *ast.TypeAnnotation + if p.current.Is(lexer.TokenColon) { + p.nextSemanticToken() + + typeBound, err = parseTypeAnnotation(p) + if err != nil { + return nil, err + } + + } + + return ast.NewTypeParameter( + p.memoryGauge, + identifier, + typeBound, + ), nil +} + +func parseFunctionDeclaration( + p *parser, + functionBlockIsOptional bool, + access ast.Access, + accessPos *ast.Position, + staticPos *ast.Position, + nativePos *ast.Position, + docString string, +) (*ast.FunctionDeclaration, error) { + + startPos := ast.EarliestPosition(p.current.StartPos, accessPos, staticPos, nativePos) + + // Skip the `fun` keyword + p.nextSemanticToken() + if !p.current.Is(lexer.TokenIdentifier) { + return nil, p.syntaxError( + "expected identifier after start of function declaration, got %s", + p.current.Type, + ) + } + + identifier := p.tokenToIdentifier(p.current) + + // Skip the identifier + p.next() + + var typeParameterList *ast.TypeParameterList + + if p.config.TypeParametersEnabled { + var err error + typeParameterList, err = parseTypeParameterList(p) + if err != nil { + return nil, err + } + } + + parameterList, returnTypeAnnotation, functionBlock, err := + parseFunctionParameterListAndRest(p, functionBlockIsOptional) + + if err != nil { + return nil, err + } + + return ast.NewFunctionDeclaration( + p.memoryGauge, + access, + ast.FunctionPurityUnspecified, + staticPos != nil, + nativePos != nil, + identifier, + typeParameterList, + parameterList, + returnTypeAnnotation, + functionBlock, + startPos, + docString, + ), nil +} + +func parseFunctionParameterListAndRest( + p *parser, + functionBlockIsOptional bool, +) ( + parameterList *ast.ParameterList, + returnTypeAnnotation *ast.TypeAnnotation, + functionBlock *ast.FunctionBlock, + err error, +) { + // Parameter list + + parameterList, err = parseParameterList(p) + if err != nil { + return + } + + // Optional return type + + current := p.current + cursor := p.tokens.Cursor() + p.skipSpaceAndComments() + if p.current.Is(lexer.TokenColon) { + // Skip the colon + p.nextSemanticToken() + + returnTypeAnnotation, err = parseTypeAnnotation(p) + if err != nil { + return + } + } else { + p.tokens.Revert(cursor) + p.current = current + } + + // (Potentially optional) block + + if functionBlockIsOptional { + current = p.current + cursor := p.tokens.Cursor() + p.skipSpaceAndComments() + if !p.current.Is(lexer.TokenBraceOpen) { + p.tokens.Revert(cursor) + p.current = current + return + } + } + + functionBlock, err = parseFunctionBlock(p) + if err != nil { + return + } + + return +} diff --git a/runtime/old_parser/invalidnumberliteralkind.go b/runtime/old_parser/invalidnumberliteralkind.go new file mode 100644 index 0000000000..7cd7d6064e --- /dev/null +++ b/runtime/old_parser/invalidnumberliteralkind.go @@ -0,0 +1,52 @@ +/* + * Cadence - The resource-oriented smart contract programming language + * + * Copyright Dapper Labs, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package old_parser + +import ( + "github.com/onflow/cadence/runtime/errors" +) + +//go:generate go run golang.org/x/tools/cmd/stringer -type=InvalidNumberLiteralKind + +type InvalidNumberLiteralKind uint + +const ( + InvalidNumberLiteralKindUnknown InvalidNumberLiteralKind = iota + InvalidNumberLiteralKindLeadingUnderscore + InvalidNumberLiteralKindTrailingUnderscore + InvalidNumberLiteralKindUnknownPrefix + InvalidNumberLiteralKindMissingDigits +) + +func (k InvalidNumberLiteralKind) Description() string { + switch k { + case InvalidNumberLiteralKindLeadingUnderscore: + return "leading underscore" + case InvalidNumberLiteralKindTrailingUnderscore: + return "trailing underscore" + case InvalidNumberLiteralKindUnknownPrefix: + return "unknown prefix" + case InvalidNumberLiteralKindMissingDigits: + return "missing digits" + case InvalidNumberLiteralKindUnknown: + return "unknown" + } + + panic(errors.NewUnreachableError()) +} diff --git a/runtime/old_parser/invalidnumberliteralkind_string.go b/runtime/old_parser/invalidnumberliteralkind_string.go new file mode 100644 index 0000000000..7f611d2dbf --- /dev/null +++ b/runtime/old_parser/invalidnumberliteralkind_string.go @@ -0,0 +1,27 @@ +// Code generated by "stringer -type=InvalidNumberLiteralKind"; DO NOT EDIT. + +package old_parser + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[InvalidNumberLiteralKindUnknown-0] + _ = x[InvalidNumberLiteralKindLeadingUnderscore-1] + _ = x[InvalidNumberLiteralKindTrailingUnderscore-2] + _ = x[InvalidNumberLiteralKindUnknownPrefix-3] + _ = x[InvalidNumberLiteralKindMissingDigits-4] +} + +const _InvalidNumberLiteralKind_name = "InvalidNumberLiteralKindUnknownInvalidNumberLiteralKindLeadingUnderscoreInvalidNumberLiteralKindTrailingUnderscoreInvalidNumberLiteralKindUnknownPrefixInvalidNumberLiteralKindMissingDigits" + +var _InvalidNumberLiteralKind_index = [...]uint8{0, 31, 72, 114, 151, 188} + +func (i InvalidNumberLiteralKind) String() string { + if i >= InvalidNumberLiteralKind(len(_InvalidNumberLiteralKind_index)-1) { + return "InvalidNumberLiteralKind(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _InvalidNumberLiteralKind_name[_InvalidNumberLiteralKind_index[i]:_InvalidNumberLiteralKind_index[i+1]] +} diff --git a/runtime/old_parser/keyword.go b/runtime/old_parser/keyword.go new file mode 100644 index 0000000000..d4e5b33af2 --- /dev/null +++ b/runtime/old_parser/keyword.go @@ -0,0 +1,71 @@ +/* + * Cadence - The resource-oriented smart contract programming language + * + * Copyright Dapper Labs, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package old_parser + +const ( + keywordIf = "if" + keywordElse = "else" + keywordWhile = "while" + keywordBreak = "break" + keywordContinue = "continue" + keywordReturn = "return" + keywordTrue = "true" + keywordFalse = "false" + keywordNil = "nil" + keywordLet = "let" + keywordVar = "var" + keywordFun = "fun" + keywordAs = "as" + keywordCreate = "create" + keywordDestroy = "destroy" + keywordFor = "for" + keywordIn = "in" + keywordEmit = "emit" + keywordAuth = "auth" + keywordPriv = "priv" + keywordPub = "pub" + keywordAccess = "access" + keywordSet = "set" + keywordAll = "all" + keywordSelf = "self" + keywordInit = "init" + keywordContract = "contract" + keywordAccount = "account" + keywordImport = "import" + keywordFrom = "from" + keywordPre = "pre" + keywordPost = "post" + keywordEvent = "event" + keywordStruct = "struct" + keywordResource = "resource" + keywordInterface = "interface" + KeywordTransaction = "transaction" + keywordPrepare = "prepare" + keywordExecute = "execute" + keywordCase = "case" + keywordSwitch = "switch" + keywordDefault = "default" + keywordEnum = "enum" + keywordAttachment = "attachment" + keywordAttach = "attach" + keywordRemove = "remove" + keywordTo = "to" + keywordStatic = "static" + keywordNative = "native" +) diff --git a/runtime/old_parser/lexer/lexer.go b/runtime/old_parser/lexer/lexer.go new file mode 100644 index 0000000000..a2b10ca306 --- /dev/null +++ b/runtime/old_parser/lexer/lexer.go @@ -0,0 +1,455 @@ +/* + * Cadence - The resource-oriented smart contract programming language + * + * Copyright Dapper Labs, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package lexer + +import ( + "fmt" + "sync" + "unicode/utf8" + + "github.com/onflow/cadence/runtime/ast" + "github.com/onflow/cadence/runtime/common" + "github.com/onflow/cadence/runtime/errors" +) + +// tokenLimit is a sensible limit for how many tokens may be emitted +const tokenLimit = 1 << 19 + +type TokenLimitReachedError struct { + ast.Position +} + +var _ error = TokenLimitReachedError{} +var _ errors.UserError = TokenLimitReachedError{} + +func (TokenLimitReachedError) IsUserError() {} + +func (TokenLimitReachedError) Error() string { + return fmt.Sprintf("limit of %d tokens exceeded", tokenLimit) +} + +type position struct { + line int + column int +} + +type lexer struct { + // memoryGauge is used for metering memory usage + memoryGauge common.MemoryGauge + // input is the entire input string + input []byte + // tokens contains all tokens of the stream + tokens []Token + // startPos is the start position of the current word + startPos position + // startOffset is the start offset of the current word in the current line + startOffset int + // endOffset is the end offset of the current word in the current line + endOffset int + // prevEndOffset is the previous end offset, used for stepping back + prevEndOffset int + // cursor is the offset in the token stream + cursor int + // tokenCount is the number of tokens in the stream + tokenCount int + // current is the currently scanned rune + current rune + // prev is the previously scanned rune, used for stepping back + prev rune + // canBackup indicates whether stepping back is allowed + canBackup bool +} + +var _ TokenStream = &lexer{} + +func (l *lexer) Next() Token { + if l.cursor >= l.tokenCount { + + // At the end of the token stream, + // emit a synthetic EOF token + + endPos := l.endPos() + pos := ast.NewPosition( + l.memoryGauge, + l.endOffset-1, + endPos.line, + endPos.column, + ) + + return Token{ + Type: TokenEOF, + Range: ast.NewRange( + l.memoryGauge, + pos, + pos, + ), + } + + } + token := l.tokens[l.cursor] + l.cursor++ + return token +} + +func (l *lexer) Input() []byte { + return l.input +} + +func (l *lexer) Cursor() int { + return l.cursor +} + +func (l *lexer) Revert(cursor int) { + l.cursor = cursor +} + +func (l *lexer) clear() { + l.startOffset = 0 + l.endOffset = 0 + l.prevEndOffset = 0 + l.current = EOF + l.prev = EOF + l.canBackup = false + l.startPos = position{line: 1} + l.cursor = 0 + l.tokens = l.tokens[:0] + l.tokenCount = 0 +} + +func (l *lexer) Reclaim() { + pool.Put(l) +} + +var pool = sync.Pool{ + New: func() any { + return &lexer{ + tokens: make([]Token, 0, 2048), + } + }, +} + +func Lex(input []byte, memoryGauge common.MemoryGauge) TokenStream { + l := pool.Get().(*lexer) + l.clear() + l.memoryGauge = memoryGauge + l.input = input + l.run(rootState) + return l +} + +// run executes the stateFn, which will scan the runes in the input +// and emit tokens. +// +// stateFn might return another stateFn to indicate further scanning work, +// or nil if there is no scanning work left to be done, +// i.e. run will keep running the returned stateFn until no more +// stateFn is returned, which for example happens when reaching the end of the file. +// +// When all stateFn have been executed, an EOF token is emitted. +func (l *lexer) run(state stateFn) { + + // catch panic exceptions, emit it to the tokens channel before + // closing it + defer func() { + if r := recover(); r != nil { + var err error + switch r := r.(type) { + case errors.MemoryError, errors.InternalError: + // fatal errors and internal errors percolates up. + // Note: not all fatal errors are internal errors. + // e.g: memory limit exceeding is a fatal error, but also a user error. + panic(r) + case error: + err = r + default: + err = fmt.Errorf("lexer: %v", r) + } + + l.emitError(err) + } + }() + + for state != nil { + state = state(l) + } +} + +// next decodes the next rune (UTF8 character) from the input string. +// +// It returns EOF if it reaches the end of the file, +// otherwise returns the scanned rune. +func (l *lexer) next() rune { + l.canBackup = true + + endOffset := l.endOffset + + // update prevEndOffset and prev so that we can step back one rune. + l.prevEndOffset = endOffset + l.prev = l.current + + r := EOF + w := 1 + if endOffset < len(l.input) { + r, w = utf8.DecodeRune(l.input[endOffset:]) + } + + l.endOffset += w + l.current = r + + return r +} + +// backupOne steps back one rune. +// Can be called only once per call of next. +func (l *lexer) backupOne() { + if !l.canBackup { + // TODO: should this be an internal error? + panic("second backup") + } + l.canBackup = false + + l.endOffset = l.prevEndOffset + l.current = l.prev +} + +func (l *lexer) word() []byte { + start := l.startOffset + end := l.endOffset + return l.input[start:end] +} + +// acceptOne reads one rune ahead. +// It returns true if the next rune matches with the input rune, +// otherwise it steps back one rune and returns false. +func (l *lexer) acceptOne(r rune) bool { + if l.next() == r { + return true + } + l.backupOne() + return false +} + +// emit writes a token to the channel. +func (l *lexer) emit(ty TokenType, spaceOrError any, rangeStart ast.Position, consume bool) { + + if len(l.tokens) >= tokenLimit { + panic(TokenLimitReachedError{}) + } + + endPos := l.endPos() + + token := Token{ + Type: ty, + SpaceOrError: spaceOrError, + Range: ast.NewRange( + l.memoryGauge, + rangeStart, + ast.NewPosition( + l.memoryGauge, + l.endOffset-1, + endPos.line, + endPos.column, + ), + ), + } + + l.tokens = append(l.tokens, token) + l.tokenCount = len(l.tokens) + + if consume { + l.startOffset = l.endOffset + + l.startPos = endPos + r, _ := utf8.DecodeRune(l.input[l.endOffset-1:]) + + if r == '\n' { + l.startPos.line++ + l.startPos.column = 0 + } else { + l.startPos.column++ + } + } +} + +func (l *lexer) startPosition() ast.Position { + return ast.NewPosition( + l.memoryGauge, + l.startOffset, + l.startPos.line, + l.startPos.column, + ) +} + +func (l *lexer) endPos() position { + startOffset := l.startOffset + endOffset := l.endOffset + + endPos := l.startPos + + var w int + for offset := startOffset; offset < endOffset-1; offset += w { + var r rune + r, w = utf8.DecodeRune(l.input[offset:]) + + if r == '\n' { + endPos.line++ + endPos.column = 0 + } else { + endPos.column++ + } + } + + return endPos +} + +func (l *lexer) emitType(ty TokenType) { + common.UseMemory(l.memoryGauge, common.TypeTokenMemoryUsage) + + l.emit(ty, nil, l.startPosition(), true) +} + +func (l *lexer) emitError(err error) { + common.UseMemory(l.memoryGauge, common.ErrorTokenMemoryUsage) + + endPos := l.endPos() + rangeStart := ast.NewPosition( + l.memoryGauge, + l.endOffset-1, + endPos.line, + endPos.column, + ) + l.emit(TokenError, err, rangeStart, false) +} + +func (l *lexer) scanSpace() (containsNewline bool) { + // lookahead is already lexed. + // parse more, if any + l.acceptWhile(func(r rune) bool { + switch r { + case ' ', '\t', '\r': + return true + case '\n': + containsNewline = true + return true + default: + return false + } + }) + return +} + +func (l *lexer) scanIdentifier() { + // lookahead is already lexed. + // parse more, if any + l.acceptWhile(func(r rune) bool { + return r >= 'a' && r <= 'z' || + r >= 'A' && r <= 'Z' || + r >= '0' && r <= '9' || + r == '_' + }) +} + +func (l *lexer) scanLineComment() { + // lookahead is already lexed. + // parse more, if any + l.acceptWhile(func(r rune) bool { + return !(r == '\n' || r == EOF) + }) +} + +func (l *lexer) acceptWhile(f func(rune) bool) { + for { + r := l.next() + + if f(r) { + continue + } + + l.backupOne() + return + } +} + +func (l *lexer) scanString(quote rune) { + r := l.next() + for r != quote { + switch r { + case '\n', EOF: + // NOTE: invalid end of string handled by parser + l.backupOne() + return + case '\\': + r = l.next() + switch r { + case '\n', EOF: + // NOTE: invalid end of string handled by parser + l.backupOne() + return + } + } + r = l.next() + } +} + +func (l *lexer) scanBinaryRemainder() { + l.acceptWhile(func(r rune) bool { + return r == '0' || r == '1' || r == '_' + }) +} + +func (l *lexer) scanOctalRemainder() { + l.acceptWhile(func(r rune) bool { + return (r >= '0' && r <= '7') || r == '_' + }) +} + +func (l *lexer) scanHexadecimalRemainder() { + l.acceptWhile(func(r rune) bool { + return (r >= '0' && r <= '9') || + (r >= 'a' && r <= 'f') || + (r >= 'A' && r <= 'F') || + r == '_' + }) +} + +func (l *lexer) scanDecimalOrFixedPointRemainder() TokenType { + l.acceptWhile(isDecimalDigitOrUnderscore) + r := l.next() + if r == '.' { + l.scanFixedPointRemainder() + return TokenFixedPointNumberLiteral + } else { + l.backupOne() + return TokenDecimalIntegerLiteral + } +} + +func (l *lexer) scanFixedPointRemainder() { + r := l.next() + if !isDecimalDigitOrUnderscore(r) { + l.backupOne() + l.emitError(fmt.Errorf("missing fractional digits")) + return + } + l.acceptWhile(isDecimalDigitOrUnderscore) +} + +func isDecimalDigitOrUnderscore(r rune) bool { + return (r >= '0' && r <= '9') || r == '_' +} diff --git a/runtime/old_parser/lexer/lexer_test.go b/runtime/old_parser/lexer/lexer_test.go new file mode 100644 index 0000000000..926271756e --- /dev/null +++ b/runtime/old_parser/lexer/lexer_test.go @@ -0,0 +1,2643 @@ +/* + * Cadence - The resource-oriented smart contract programming language + * + * Copyright Dapper Labs, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package lexer + +import ( + "errors" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/goleak" + + "github.com/onflow/cadence/runtime/ast" + "github.com/onflow/cadence/runtime/tests/utils" +) + +func TestMain(m *testing.M) { + goleak.VerifyTestMain(m) +} + +func withTokens(tokenStream TokenStream, fn func([]Token)) { + tokens := make([]Token, 0) + for { + token := tokenStream.Next() + tokens = append(tokens, token) + if token.Is(TokenEOF) { + fn(tokens) + return + } + } +} + +type token struct { + Source string + Token +} + +func testLex(t *testing.T, input string, expected []token) { + + t.Parallel() + + expectedTokens := make([]Token, len(expected)) + for i, e := range expected { + expectedTokens[i] = e.Token + } + + bytes := []byte(input) + + withTokens(Lex(bytes, nil), func(actualTokens []Token) { + utils.AssertEqualWithDiff(t, expectedTokens, actualTokens) + + require.Len(t, actualTokens, len(expectedTokens)) + for i, expectedToken := range expected { + actualToken := actualTokens[i] + if actualToken.Type == TokenEOF { + continue + } + assert.Equal(t, + expectedToken.Source, + string(actualToken.Source(bytes)), + ) + } + }) +} + +func TestLexBasic(t *testing.T) { + + t.Parallel() + + t.Run("two numbers separated by whitespace", func(t *testing.T) { + testLex(t, + " 01\t 10", + []token{ + { + Token: Token{ + Type: TokenSpace, + SpaceOrError: Space{ + ContainsNewline: false, + }, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + Source: " ", + }, + { + Token: Token{ + Type: TokenDecimalIntegerLiteral, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + EndPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + }, + }, + Source: "01", + }, + { + Token: Token{ + Type: TokenSpace, + SpaceOrError: Space{ + ContainsNewline: false, + }, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 3, Offset: 3}, + EndPos: ast.Position{Line: 1, Column: 5, Offset: 5}, + }, + }, + Source: "\t ", + }, + { + Token: Token{ + Type: TokenDecimalIntegerLiteral, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 6, Offset: 6}, + EndPos: ast.Position{Line: 1, Column: 7, Offset: 7}, + }, + }, + Source: "10", + }, + { + Token: Token{ + Type: TokenEOF, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 8, Offset: 8}, + EndPos: ast.Position{Line: 1, Column: 8, Offset: 8}, + }, + }, + }, + }, + ) + }) + + t.Run("assignment", func(t *testing.T) { + testLex(t, + "x=1", + []token{ + { + Token: Token{ + Type: TokenIdentifier, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + Source: "x", + }, + { + Token: Token{ + Type: TokenEqual, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + EndPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + }, + }, + Source: "=", + }, + { + Token: Token{ + Type: TokenDecimalIntegerLiteral, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + EndPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + }, + }, + Source: "1", + }, + { + Token: Token{ + Type: TokenEOF, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 3, Offset: 3}, + EndPos: ast.Position{Line: 1, Column: 3, Offset: 3}, + }, + }, + }, + }, + ) + }) + + t.Run("simple arithmetic: plus and times", func(t *testing.T) { + testLex(t, + "(2 + 3) * 4", + []token{ + { + Token: Token{ + Type: TokenParenOpen, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + Source: "(", + }, + { + Token: Token{ + Type: TokenDecimalIntegerLiteral, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + EndPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + }, + }, + Source: "2", + }, + { + Token: Token{ + Type: TokenSpace, + SpaceOrError: Space{ + ContainsNewline: false, + }, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + EndPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + }, + }, + Source: " ", + }, + { + Token: Token{ + Type: TokenPlus, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 3, Offset: 3}, + EndPos: ast.Position{Line: 1, Column: 3, Offset: 3}, + }, + }, + Source: "+", + }, + { + Token: Token{ + Type: TokenSpace, + SpaceOrError: Space{ + ContainsNewline: false, + }, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + EndPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + }, + }, + Source: " ", + }, + { + Token: Token{ + Type: TokenDecimalIntegerLiteral, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 5, Offset: 5}, + EndPos: ast.Position{Line: 1, Column: 5, Offset: 5}, + }, + }, + Source: "3", + }, + { + Token: Token{ + Type: TokenParenClose, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 6, Offset: 6}, + EndPos: ast.Position{Line: 1, Column: 6, Offset: 6}, + }, + }, + Source: ")", + }, + { + Token: Token{ + Type: TokenSpace, + SpaceOrError: Space{ + ContainsNewline: false, + }, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 7, Offset: 7}, + EndPos: ast.Position{Line: 1, Column: 7, Offset: 7}, + }, + }, + Source: " ", + }, + { + Token: Token{ + Type: TokenStar, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 8, Offset: 8}, + EndPos: ast.Position{Line: 1, Column: 8, Offset: 8}, + }, + }, + Source: "*", + }, + { + Token: Token{ + Type: TokenSpace, + SpaceOrError: Space{ + ContainsNewline: false, + }, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 9, Offset: 9}, + EndPos: ast.Position{Line: 1, Column: 9, Offset: 9}, + }, + }, + Source: " ", + }, + { + Token: Token{ + Type: TokenDecimalIntegerLiteral, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 10, Offset: 10}, + EndPos: ast.Position{Line: 1, Column: 10, Offset: 10}, + }, + }, + Source: "4", + }, + { + Token: Token{ + Type: TokenEOF, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 11, Offset: 11}, + EndPos: ast.Position{Line: 1, Column: 11, Offset: 11}, + }, + }, + }, + }, + ) + }) + + t.Run("simple arithmetic: minus and div", func(t *testing.T) { + testLex(t, + "(2 - 3) / 4", + []token{ + { + Token: Token{ + Type: TokenParenOpen, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + Source: "(", + }, + { + Token: Token{ + Type: TokenDecimalIntegerLiteral, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + EndPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + }, + }, + Source: "2", + }, + { + Token: Token{ + Type: TokenSpace, + SpaceOrError: Space{ + ContainsNewline: false, + }, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + EndPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + }, + }, + Source: " ", + }, + { + Token: Token{ + Type: TokenMinus, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 3, Offset: 3}, + EndPos: ast.Position{Line: 1, Column: 3, Offset: 3}, + }, + }, + Source: "-", + }, + { + Token: Token{ + Type: TokenSpace, + SpaceOrError: Space{ + ContainsNewline: false, + }, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + EndPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + }, + }, + Source: " ", + }, + { + Token: Token{ + Type: TokenDecimalIntegerLiteral, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 5, Offset: 5}, + EndPos: ast.Position{Line: 1, Column: 5, Offset: 5}, + }, + }, + Source: "3", + }, + { + Token: Token{ + Type: TokenParenClose, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 6, Offset: 6}, + EndPos: ast.Position{Line: 1, Column: 6, Offset: 6}, + }, + }, + Source: ")", + }, + { + Token: Token{ + Type: TokenSpace, + SpaceOrError: Space{ + ContainsNewline: false, + }, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 7, Offset: 7}, + EndPos: ast.Position{Line: 1, Column: 7, Offset: 7}, + }, + }, + Source: " ", + }, + { + Token: Token{ + Type: TokenSlash, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 8, Offset: 8}, + EndPos: ast.Position{Line: 1, Column: 8, Offset: 8}, + }, + }, + Source: "/", + }, + { + Token: Token{ + Type: TokenSpace, + SpaceOrError: Space{ + ContainsNewline: false, + }, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 9, Offset: 9}, + EndPos: ast.Position{Line: 1, Column: 9, Offset: 9}, + }, + }, + Source: " ", + }, + { + Token: Token{ + Type: TokenDecimalIntegerLiteral, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 10, Offset: 10}, + EndPos: ast.Position{Line: 1, Column: 10, Offset: 10}, + }, + }, + Source: "4", + }, + { + Token: Token{ + Type: TokenEOF, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 11, Offset: 11}, + EndPos: ast.Position{Line: 1, Column: 11, Offset: 11}, + }, + }, + }, + }, + ) + }) + + t.Run("multiple lines", func(t *testing.T) { + testLex(t, + "1 \n 2\n", + []token{ + { + Token: Token{ + Type: TokenDecimalIntegerLiteral, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + Source: "1", + }, + { + Token: Token{ + Type: TokenSpace, + SpaceOrError: Space{ + ContainsNewline: true, + }, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + EndPos: ast.Position{Line: 2, Column: 1, Offset: 4}, + }, + }, + Source: " \n ", + }, + { + Token: Token{ + Type: TokenDecimalIntegerLiteral, + Range: ast.Range{ + StartPos: ast.Position{Line: 2, Column: 2, Offset: 5}, + EndPos: ast.Position{Line: 2, Column: 2, Offset: 5}, + }, + }, + Source: "2", + }, + { + Token: Token{ + Type: TokenSpace, + SpaceOrError: Space{ + ContainsNewline: true, + }, + Range: ast.Range{ + StartPos: ast.Position{Line: 2, Column: 3, Offset: 6}, + EndPos: ast.Position{Line: 2, Column: 3, Offset: 6}, + }, + }, + Source: "\n", + }, + { + Token: Token{ + Type: TokenEOF, + Range: ast.Range{ + StartPos: ast.Position{Line: 3, Column: 0, Offset: 7}, + EndPos: ast.Position{Line: 3, Column: 0, Offset: 7}, + }, + }, + }, + }, + ) + }) + + t.Run("nil-coalesce", func(t *testing.T) { + testLex(t, + "1 ?? 2", + []token{ + { + Token: Token{ + Type: TokenDecimalIntegerLiteral, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + Source: "1", + }, + { + Token: Token{ + Type: TokenSpace, + SpaceOrError: Space{ + ContainsNewline: false, + }, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + EndPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + }, + }, + Source: " ", + }, + { + Token: Token{ + Type: TokenDoubleQuestionMark, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + EndPos: ast.Position{Line: 1, Column: 3, Offset: 3}, + }, + }, + Source: "??", + }, + { + Token: Token{ + Type: TokenSpace, + SpaceOrError: Space{ + ContainsNewline: false, + }, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + EndPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + }, + }, + Source: " ", + }, + { + Token: Token{ + Type: TokenDecimalIntegerLiteral, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 5, Offset: 5}, + EndPos: ast.Position{Line: 1, Column: 5, Offset: 5}, + }, + }, + Source: "2", + }, + { + Token: Token{ + Type: TokenEOF, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 6, Offset: 6}, + EndPos: ast.Position{Line: 1, Column: 6, Offset: 6}, + }, + }, + }, + }, + ) + }) + + t.Run("identifier", func(t *testing.T) { + testLex(t, + "test", + []token{ + { + Token: Token{ + Type: TokenIdentifier, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 3, Offset: 3}, + }, + }, + Source: "test", + }, + { + Token: Token{ + Type: TokenEOF, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + EndPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + }, + }, + }, + }, + ) + }) + + t.Run("identifier with leading underscore and trailing numbers", func(t *testing.T) { + testLex(t, + "_test_123", + []token{ + { + Token: Token{ + Type: TokenIdentifier, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 8, Offset: 8}, + }, + }, + Source: "_test_123", + }, + { + Token: Token{ + Type: TokenEOF, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 9, Offset: 9}, + EndPos: ast.Position{Line: 1, Column: 9, Offset: 9}, + }, + }, + }, + }, + ) + }) + + t.Run("colon, comma, semicolon, question mark", func(t *testing.T) { + testLex(t, + ":,;.?", + + []token{ + { + Token: Token{ + Type: TokenColon, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + Source: ":", + }, + { + Token: Token{ + Type: TokenComma, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + EndPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + }, + }, + Source: ",", + }, + { + Token: Token{ + Type: TokenSemicolon, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + EndPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + }, + }, + Source: ";", + }, + { + Token: Token{ + Type: TokenDot, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 3, Offset: 3}, + EndPos: ast.Position{Line: 1, Column: 3, Offset: 3}, + }, + }, + Source: ".", + }, + { + Token: Token{ + Type: TokenQuestionMark, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + EndPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + }, + }, + Source: "?", + }, + { + Token: Token{ + Type: TokenEOF, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 5, Offset: 5}, + EndPos: ast.Position{Line: 1, Column: 5, Offset: 5}, + }, + }, + }, + }, + ) + }) + + t.Run("brackets and braces", func(t *testing.T) { + testLex(t, + "[}]{", + []token{ + { + Token: Token{ + Type: TokenBracketOpen, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + Source: "[", + }, + { + Token: Token{ + Type: TokenBraceClose, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + EndPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + }, + }, + Source: "}", + }, + { + Token: Token{ + Type: TokenBracketClose, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + EndPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + }, + }, + Source: "]", + }, + { + Token: Token{ + Type: TokenBraceOpen, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 3, Offset: 3}, + EndPos: ast.Position{Line: 1, Column: 3, Offset: 3}, + }, + }, + Source: "{", + }, + { + Token: Token{ + Type: TokenEOF, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + EndPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + }, + }, + }, + }, + ) + }) + + t.Run("comparisons", func(t *testing.T) { + testLex(t, + "=<><-<=>=", + []token{ + { + Token: Token{ + Type: TokenEqual, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + Source: "=", + }, + { + Token: Token{ + Type: TokenLess, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + EndPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + }, + }, + Source: "<", + }, + { + Token: Token{ + Type: TokenGreater, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + EndPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + }, + }, + Source: ">", + }, + { + Token: Token{ + Type: TokenLeftArrow, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 3, Offset: 3}, + EndPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + }, + }, + Source: "<-", + }, + { + Token: Token{ + Type: TokenLessEqual, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 5, Offset: 5}, + EndPos: ast.Position{Line: 1, Column: 6, Offset: 6}, + }, + }, + Source: "<=", + }, + { + Token: Token{ + Type: TokenGreaterEqual, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 7, Offset: 7}, + EndPos: ast.Position{Line: 1, Column: 8, Offset: 8}, + }, + }, + Source: ">=", + }, + { + Token: Token{ + Type: TokenEOF, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 9, Offset: 9}, + EndPos: ast.Position{Line: 1, Column: 9, Offset: 9}, + }, + }, + }, + }, + ) + }) +} + +func TestLexString(t *testing.T) { + + t.Parallel() + + t.Run("valid, empty", func(t *testing.T) { + testLex(t, + `""`, + []token{ + { + Token: Token{ + Type: TokenString, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + }, + }, + Source: `""`, + }, + { + Token: Token{ + Type: TokenEOF, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + EndPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + }, + }, + }, + }, + ) + }) + + t.Run("valid, non-empty", func(t *testing.T) { + testLex(t, + `"test"`, + []token{ + { + Token: Token{ + Type: TokenString, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 5, Offset: 5}, + }, + }, + Source: `"test"`, + }, + { + Token: Token{ + Type: TokenEOF, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 6, Offset: 6}, + EndPos: ast.Position{Line: 1, Column: 6, Offset: 6}, + }, + }, + }, + }, + ) + }) + + t.Run("valid, with valid tab escape", func(t *testing.T) { + testLex(t, + `"te\tst"`, + []token{ + { + Token: Token{ + Type: TokenString, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 7, Offset: 7}, + }, + }, + Source: `"te\tst"`, + }, + { + Token: Token{ + Type: TokenEOF, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 8, Offset: 8}, + EndPos: ast.Position{Line: 1, Column: 8, Offset: 8}, + }, + }, + }, + }, + ) + }) + + t.Run("valid, with invalid escape character", func(t *testing.T) { + testLex(t, + `"te\Xst"`, + []token{ + { + Token: Token{ + Type: TokenString, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 7, Offset: 7}, + }, + }, + Source: `"te\Xst"`, + }, + { + Token: Token{ + Type: TokenEOF, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 8, Offset: 8}, + EndPos: ast.Position{Line: 1, Column: 8, Offset: 8}, + }, + }, + }, + }, + ) + }) + + t.Run("valid, with valid quote escape", func(t *testing.T) { + testLex(t, + `"te\"st"`, + []token{ + { + Token: Token{ + Type: TokenString, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 7, Offset: 7}, + }, + }, + Source: `"te\"st"`, + }, + { + Token: Token{ + Type: TokenEOF, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 8, Offset: 8}, + EndPos: ast.Position{Line: 1, Column: 8, Offset: 8}, + }, + }, + }, + }, + ) + }) + + t.Run("invalid, empty, not terminated at line end", func(t *testing.T) { + testLex(t, + "\"\n", + []token{ + { + Token: Token{ + Type: TokenString, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + Source: "\"", + }, + { + Token: Token{ + Type: TokenSpace, + SpaceOrError: Space{ + ContainsNewline: true, + }, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + EndPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + }, + }, + Source: "\n", + }, + { + Token: Token{ + Type: TokenEOF, + Range: ast.Range{ + StartPos: ast.Position{Line: 2, Column: 0, Offset: 2}, + EndPos: ast.Position{Line: 2, Column: 0, Offset: 2}, + }, + }, + }, + }, + ) + }) + + t.Run("invalid, non-empty, not terminated at line end", func(t *testing.T) { + testLex(t, + "\"te\n", + []token{ + { + Token: Token{ + Type: TokenString, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + }, + }, + Source: "\"te", + }, + { + Token: Token{ + Type: TokenSpace, + SpaceOrError: Space{ + ContainsNewline: true, + }, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 3, Offset: 3}, + EndPos: ast.Position{Line: 1, Column: 3, Offset: 3}, + }, + }, + Source: "\n", + }, + { + Token: Token{ + Type: TokenEOF, + Range: ast.Range{ + StartPos: ast.Position{Line: 2, Column: 0, Offset: 4}, + EndPos: ast.Position{Line: 2, Column: 0, Offset: 4}, + }, + }, + }, + }, + ) + }) + + t.Run("invalid, empty, not terminated at end of file", func(t *testing.T) { + testLex(t, + "\"", + []token{ + { + Token: Token{ + Type: TokenString, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + Source: "\"", + }, + { + Token: Token{ + Type: TokenEOF, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + EndPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + }, + }, + }, + }, + ) + }) + + t.Run("invalid, non-empty, not terminated at end of file", func(t *testing.T) { + testLex(t, + "\"te", + []token{ + { + Token: Token{ + Type: TokenString, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + }, + }, + Source: "\"te", + }, + { + Token: Token{ + Type: TokenEOF, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 3, Offset: 3}, + EndPos: ast.Position{Line: 1, Column: 3, Offset: 3}, + }, + }, + }, + }, + ) + }) + + t.Run("invalid, missing escape character", func(t *testing.T) { + testLex(t, + "\"\\\n", + []token{ + { + Token: Token{ + Type: TokenString, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + }, + }, + Source: "\"\\", + }, + { + Token: Token{ + Type: TokenSpace, + SpaceOrError: Space{ + ContainsNewline: true, + }, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + EndPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + }, + }, + Source: "\n", + }, + { + Token: Token{ + Type: TokenEOF, + Range: ast.Range{ + StartPos: ast.Position{Line: 2, Column: 0, Offset: 3}, + EndPos: ast.Position{Line: 2, Column: 0, Offset: 3}, + }, + }, + }, + }, + ) + }) +} + +func TestLexBlockComment(t *testing.T) { + + t.Parallel() + + t.Run("nested 1", func(t *testing.T) { + testLex(t, + `/* // *X /* \\* */`, + []token{ + { + Token: Token{ + Type: TokenBlockCommentStart, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + }, + }, + Source: "/*", + }, + { + Token: Token{ + Type: TokenBlockCommentContent, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + EndPos: ast.Position{Line: 1, Column: 9, Offset: 9}, + }, + }, + Source: ` // *X `, + }, + { + Token: Token{ + Type: TokenBlockCommentStart, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 10, Offset: 10}, + EndPos: ast.Position{Line: 1, Column: 11, Offset: 11}, + }, + }, + Source: "/*", + }, + { + Token: Token{ + Type: TokenBlockCommentContent, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 12, Offset: 12}, + EndPos: ast.Position{Line: 1, Column: 17, Offset: 17}, + }, + }, + Source: ` \\* `, + }, + { + Token: Token{ + Type: TokenBlockCommentEnd, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 18, Offset: 18}, + EndPos: ast.Position{Line: 1, Column: 19, Offset: 19}, + }, + }, + Source: "*/", + }, + { + Token: Token{ + Type: TokenEOF, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 20, Offset: 20}, + EndPos: ast.Position{Line: 1, Column: 20, Offset: 20}, + }, + }, + }, + }, + ) + }) + + t.Run("nested 2", func(t *testing.T) { + testLex(t, + `/* test foo /* bar */ asd */ `, + []token{ + { + Token: Token{ + Type: TokenBlockCommentStart, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + }, + }, + Source: "/*", + }, + { + Token: Token{ + Type: TokenBlockCommentContent, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + EndPos: ast.Position{Line: 1, Column: 11, Offset: 11}, + }, + }, + Source: ` test foo `, + }, + { + Token: Token{ + Type: TokenBlockCommentStart, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 12, Offset: 12}, + EndPos: ast.Position{Line: 1, Column: 13, Offset: 13}, + }, + }, + Source: "/*", + }, + { + Token: Token{ + Type: TokenBlockCommentContent, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 14, Offset: 14}, + EndPos: ast.Position{Line: 1, Column: 18, Offset: 18}, + }, + }, + Source: ` bar `, + }, + { + Token: Token{ + Type: TokenBlockCommentEnd, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 19, Offset: 19}, + EndPos: ast.Position{Line: 1, Column: 20, Offset: 20}, + }, + }, + Source: "*/", + }, + { + Token: Token{ + Type: TokenBlockCommentContent, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 21, Offset: 21}, + EndPos: ast.Position{Line: 1, Column: 25, Offset: 25}, + }, + }, + Source: ` asd `, + }, + { + Token: Token{ + Type: TokenBlockCommentEnd, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 26, Offset: 26}, + EndPos: ast.Position{Line: 1, Column: 27, Offset: 27}, + }, + }, + Source: "*/", + }, + { + Token: Token{ + Type: TokenSpace, + SpaceOrError: Space{ + ContainsNewline: false, + }, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 28, Offset: 28}, + EndPos: ast.Position{Line: 1, Column: 29, Offset: 29}, + }, + }, + Source: " ", + }, + { + Token: Token{ + Type: TokenEOF, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 30, Offset: 30}, + EndPos: ast.Position{Line: 1, Column: 30, Offset: 30}, + }, + }, + }, + }, + ) + }) +} + +func TestLexIntegerLiterals(t *testing.T) { + + t.Parallel() + + t.Run("binary prefix, missing trailing digits", func(t *testing.T) { + testLex(t, + `0b`, + []token{ + { + Token: Token{ + Type: TokenError, + SpaceOrError: errors.New("missing digits"), + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + EndPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + }, + }, + Source: "b", + }, + { + Token: Token{ + Type: TokenBinaryIntegerLiteral, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + }, + }, + Source: "0b", + }, + { + Token: Token{ + Type: TokenEOF, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + EndPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + }, + }, + }, + }, + ) + }) + + t.Run("binary", func(t *testing.T) { + testLex(t, + `0b101010`, + []token{ + { + Token: Token{ + Type: TokenBinaryIntegerLiteral, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 7, Offset: 7}, + }, + }, + Source: "0b101010", + }, + { + Token: Token{ + Type: TokenEOF, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 8, Offset: 8}, + EndPos: ast.Position{Line: 1, Column: 8, Offset: 8}, + }, + }, + }, + }, + ) + }) + + t.Run("binary with leading zeros", func(t *testing.T) { + testLex(t, + `0b001000`, + []token{ + { + Token: Token{ + Type: TokenBinaryIntegerLiteral, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 7, Offset: 7}, + }, + }, + Source: "0b001000", + }, + { + Token: Token{ + Type: TokenEOF, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 8, Offset: 8}, + EndPos: ast.Position{Line: 1, Column: 8, Offset: 8}, + }, + }, + }, + }, + ) + }) + + t.Run("binary with underscores", func(t *testing.T) { + testLex(t, + `0b101010_101010`, + []token{ + { + Token: Token{ + Type: TokenBinaryIntegerLiteral, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 14, Offset: 14}, + }, + }, + Source: "0b101010_101010", + }, + { + Token: Token{ + Type: TokenEOF, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 15, Offset: 15}, + EndPos: ast.Position{Line: 1, Column: 15, Offset: 15}, + }, + }, + }, + }, + ) + }) + + t.Run("binary with leading underscore", func(t *testing.T) { + testLex(t, + `0b_101010_101010`, + []token{ + { + Token: Token{ + Type: TokenBinaryIntegerLiteral, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 15, Offset: 15}, + }, + }, + Source: "0b_101010_101010", + }, + { + Token: Token{ + Type: TokenEOF, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 16, Offset: 16}, + EndPos: ast.Position{Line: 1, Column: 16, Offset: 16}, + }, + }, + }, + }, + ) + }) + + t.Run("binary with trailing underscore", func(t *testing.T) { + testLex(t, + `0b101010_101010_`, + []token{ + { + Token: Token{ + Type: TokenBinaryIntegerLiteral, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 15, Offset: 15}, + }, + }, + Source: "0b101010_101010_", + }, + { + Token: Token{ + Type: TokenEOF, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 16, Offset: 16}, + EndPos: ast.Position{Line: 1, Column: 16, Offset: 16}, + }, + }, + }, + }, + ) + }) + + t.Run("octal prefix, missing trailing digits", func(t *testing.T) { + testLex(t, + `0o`, + []token{ + { + Token: Token{ + Type: TokenError, + SpaceOrError: errors.New("missing digits"), + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + EndPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + }, + }, + Source: "o", + }, + { + Token: Token{ + Type: TokenOctalIntegerLiteral, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + }, + }, + Source: "0o", + }, + { + Token: Token{ + Type: TokenEOF, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + EndPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + }, + }, + }, + }, + ) + }) + + t.Run("octal", func(t *testing.T) { + testLex(t, + `0o32`, + []token{ + { + Token: Token{ + Type: TokenOctalIntegerLiteral, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 3, Offset: 3}, + }, + }, + Source: "0o32", + }, + { + Token: Token{ + Type: TokenEOF, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + EndPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + }, + }, + }, + }, + ) + }) + + t.Run("octal with underscores", func(t *testing.T) { + testLex(t, + `0o32_45`, + []token{ + { + Token: Token{ + Type: TokenOctalIntegerLiteral, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 6, Offset: 6}, + }, + }, + Source: "0o32_45", + }, + { + Token: Token{ + Type: TokenEOF, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 7, Offset: 7}, + EndPos: ast.Position{Line: 1, Column: 7, Offset: 7}, + }, + }, + }, + }, + ) + }) + + t.Run("octal with leading underscore", func(t *testing.T) { + testLex(t, + `0o_32_45`, + []token{ + { + Token: Token{ + Type: TokenOctalIntegerLiteral, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 7, Offset: 7}, + }, + }, + Source: "0o_32_45", + }, + { + Token: Token{ + Type: TokenEOF, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 8, Offset: 8}, + EndPos: ast.Position{Line: 1, Column: 8, Offset: 8}, + }, + }, + }, + }, + ) + }) + + t.Run("octal with trailing underscore", func(t *testing.T) { + testLex(t, + `0o32_45_`, + []token{ + { + Token: Token{ + Type: TokenOctalIntegerLiteral, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 7, Offset: 7}, + }, + }, + Source: "0o32_45_", + }, + { + Token: Token{ + Type: TokenEOF, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 8, Offset: 8}, + EndPos: ast.Position{Line: 1, Column: 8, Offset: 8}, + }, + }, + }, + }, + ) + }) + + t.Run("decimal", func(t *testing.T) { + testLex(t, + `1234567890`, + []token{ + { + Token: Token{ + Type: TokenDecimalIntegerLiteral, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 9, Offset: 9}, + }, + }, + Source: "1234567890", + }, + { + Token: Token{ + Type: TokenEOF, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 10, Offset: 10}, + EndPos: ast.Position{Line: 1, Column: 10, Offset: 10}, + }, + }, + }, + }, + ) + }) + + t.Run("decimal with underscores", func(t *testing.T) { + testLex(t, + `1_234_567_890`, + []token{ + { + Token: Token{ + Type: TokenDecimalIntegerLiteral, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 12, Offset: 12}, + }, + }, + Source: "1_234_567_890", + }, + { + Token: Token{ + Type: TokenEOF, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 13, Offset: 13}, + EndPos: ast.Position{Line: 1, Column: 13, Offset: 13}, + }, + }, + }, + }, + ) + }) + + t.Run("decimal with trailing underscore", func(t *testing.T) { + testLex(t, + `1_234_567_890_`, + []token{ + { + Token: Token{ + Type: TokenDecimalIntegerLiteral, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 13, Offset: 13}, + }, + }, + Source: "1_234_567_890_", + }, + { + Token: Token{ + Type: TokenEOF, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 14, Offset: 14}, + EndPos: ast.Position{Line: 1, Column: 14, Offset: 14}, + }, + }, + }, + }, + ) + }) + + t.Run("hexadecimal prefix, missing trailing digits", func(t *testing.T) { + testLex(t, + `0x`, + []token{ + { + Token: Token{ + Type: TokenError, + SpaceOrError: errors.New("missing digits"), + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + EndPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + }, + }, + Source: "x", + }, + { + Token: Token{ + Type: TokenHexadecimalIntegerLiteral, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + }, + }, + Source: "0x", + }, + { + Token: Token{ + Type: TokenEOF, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + EndPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + }, + }, + }, + }, + ) + }) + + t.Run("hexadecimal", func(t *testing.T) { + testLex(t, + `0xf2`, + []token{ + { + Token: Token{ + Type: TokenHexadecimalIntegerLiteral, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 3, Offset: 3}, + }, + }, + Source: "0xf2", + }, + { + Token: Token{ + Type: TokenEOF, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + EndPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + }, + }, + }, + }, + ) + }) + + t.Run("hexadecimal with underscores", func(t *testing.T) { + testLex(t, + `0xf2_09`, + []token{ + { + Token: Token{ + Type: TokenHexadecimalIntegerLiteral, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 6, Offset: 6}, + }, + }, + Source: "0xf2_09", + }, + { + Token: Token{ + Type: TokenEOF, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 7, Offset: 7}, + EndPos: ast.Position{Line: 1, Column: 7, Offset: 7}, + }, + }, + }, + }, + ) + }) + + t.Run("hexadecimal with leading underscore", func(t *testing.T) { + testLex(t, + `0x_f2_09`, + []token{ + { + Token: Token{ + Type: TokenHexadecimalIntegerLiteral, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 7, Offset: 7}, + }, + }, + Source: "0x_f2_09", + }, + { + Token: Token{ + Type: TokenEOF, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 8, Offset: 8}, + EndPos: ast.Position{Line: 1, Column: 8, Offset: 8}, + }, + }, + }, + }, + ) + }) + + t.Run("hexadecimal with trailing underscore", func(t *testing.T) { + testLex(t, + `0xf2_09_`, + []token{ + { + Token: Token{ + Type: TokenHexadecimalIntegerLiteral, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 7, Offset: 7}, + }, + }, + Source: "0xf2_09_", + }, + { + Token: Token{ + Type: TokenEOF, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 8, Offset: 8}, + EndPos: ast.Position{Line: 1, Column: 8, Offset: 8}, + }, + }, + }, + }, + ) + }) + + t.Run("0", func(t *testing.T) { + testLex(t, + "0", + []token{ + { + Token: Token{ + Type: TokenDecimalIntegerLiteral, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + Source: "0", + }, + { + Token: Token{ + Type: TokenEOF, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + EndPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + }, + }, + }, + }, + ) + }) + + t.Run("01", func(t *testing.T) { + testLex(t, + "01", + []token{ + { + Token: Token{ + Type: TokenDecimalIntegerLiteral, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + }, + }, + Source: "01", + }, + { + Token: Token{ + Type: TokenEOF, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + EndPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + }, + }, + }, + }, + ) + }) + + t.Run("whitespace after 0", func(t *testing.T) { + testLex(t, + "0\n", + []token{ + { + Token: Token{ + Type: TokenDecimalIntegerLiteral, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + Source: "0", + }, + { + Token: Token{ + Type: TokenSpace, + SpaceOrError: Space{ + ContainsNewline: true, + }, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + EndPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + }, + }, + Source: "\n", + }, + { + Token: Token{ + Type: TokenEOF, + Range: ast.Range{ + StartPos: ast.Position{Line: 2, Column: 0, Offset: 2}, + EndPos: ast.Position{Line: 2, Column: 0, Offset: 2}, + }, + }, + }, + }, + ) + }) + + t.Run("leading zeros", func(t *testing.T) { + testLex(t, + "00123", + []token{ + { + Token: Token{ + Type: TokenDecimalIntegerLiteral, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + }, + }, + Source: "00123", + }, + { + Token: Token{ + Type: TokenEOF, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 5, Offset: 5}, + EndPos: ast.Position{Line: 1, Column: 5, Offset: 5}, + }, + }, + }, + }, + ) + }) + + t.Run("invalid prefix", func(t *testing.T) { + testLex(t, + "0z123", + []token{ + { + Token: Token{ + Type: TokenError, + SpaceOrError: errors.New("invalid number literal prefix: 'z'"), + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + EndPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + }, + }, + Source: "z", + }, + { + Token: Token{ + Type: TokenUnknownBaseIntegerLiteral, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + }, + }, + Source: "0z123", + }, + { + Token: Token{ + Type: TokenEOF, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 5, Offset: 5}, + EndPos: ast.Position{Line: 1, Column: 5, Offset: 5}, + }, + }, + }, + }, + ) + }) + + t.Run("leading zero and underscore", func(t *testing.T) { + + testLex(t, + "0_100", + []token{ + { + Token: Token{ + Type: TokenDecimalIntegerLiteral, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + }, + }, + Source: "0_100", + }, + { + Token: Token{ + Type: TokenEOF, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 5, Offset: 5}, + EndPos: ast.Position{Line: 1, Column: 5, Offset: 5}, + }, + }, + }, + }, + ) + }) + + t.Run("leading one and underscore", func(t *testing.T) { + + testLex(t, + "1_100", + []token{ + { + Token: Token{ + Type: TokenDecimalIntegerLiteral, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + }, + }, + Source: "1_100", + }, + { + Token: Token{ + Type: TokenEOF, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 5, Offset: 5}, + EndPos: ast.Position{Line: 1, Column: 5, Offset: 5}, + }, + }, + }, + }, + ) + }) +} + +func TestLexFixedPoint(t *testing.T) { + + t.Parallel() + + t.Run("with underscores", func(t *testing.T) { + testLex(t, + "1234_5678_90.0009_8765_4321", + []token{ + { + Token: Token{ + Type: TokenFixedPointNumberLiteral, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 26, Offset: 26}, + }, + }, + Source: "1234_5678_90.0009_8765_4321", + }, + { + Token: Token{ + Type: TokenEOF, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 27, Offset: 27}, + EndPos: ast.Position{Line: 1, Column: 27, Offset: 27}, + }, + }, + }, + }, + ) + }) + + t.Run("leading zero", func(t *testing.T) { + testLex(t, + "0.1", + []token{ + { + Token: Token{ + Type: TokenFixedPointNumberLiteral, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + }, + }, + Source: "0.1", + }, + { + Token: Token{ + Type: TokenEOF, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 3, Offset: 3}, + EndPos: ast.Position{Line: 1, Column: 3, Offset: 3}, + }, + }, + }, + }, + ) + }) + + t.Run("missing fractional digits", func(t *testing.T) { + testLex(t, + "0.", + []token{ + { + Token: Token{ + Type: TokenError, + SpaceOrError: errors.New("missing fractional digits"), + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + EndPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + }, + }, + Source: ".", + }, + { + Token: Token{ + Type: TokenFixedPointNumberLiteral, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + }, + }, + Source: "0.", + }, + { + Token: Token{ + Type: TokenEOF, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + EndPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + }, + }, + }, + }, + ) + }) +} + +func TestLexLineComment(t *testing.T) { + + t.Parallel() + + t.Run("no newline", func(t *testing.T) { + + testLex(t, + ` foo // bar `, + []token{ + { + Token: Token{ + Type: TokenSpace, + SpaceOrError: Space{ + ContainsNewline: false, + }, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + Source: " ", + }, + { + Token: Token{ + Type: TokenIdentifier, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + EndPos: ast.Position{Line: 1, Column: 3, Offset: 3}, + }, + }, + Source: "foo", + }, + { + Token: Token{ + Type: TokenSpace, + SpaceOrError: Space{ + ContainsNewline: false, + }, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + EndPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + }, + }, + Source: " ", + }, + { + Token: Token{ + Type: TokenLineComment, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 5, Offset: 5}, + EndPos: ast.Position{Line: 1, Column: 11, Offset: 11}, + }, + }, + Source: "// bar ", + }, + { + Token: Token{ + Type: TokenEOF, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 12, Offset: 12}, + EndPos: ast.Position{Line: 1, Column: 12, Offset: 12}, + }, + }, + }, + }, + ) + }) + + t.Run("newline", func(t *testing.T) { + + testLex( + t, + " foo // bar \n baz", + []token{ + { + Token: Token{ + Type: TokenSpace, + SpaceOrError: Space{ + ContainsNewline: false, + }, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + Source: " ", + }, + { + Token: Token{ + Type: TokenIdentifier, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + EndPos: ast.Position{Line: 1, Column: 3, Offset: 3}, + }, + }, + Source: "foo", + }, + { + Token: Token{ + Type: TokenSpace, + SpaceOrError: Space{ + ContainsNewline: false, + }, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + EndPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + }, + }, + Source: " ", + }, + { + Token: Token{ + Type: TokenLineComment, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 5, Offset: 5}, + EndPos: ast.Position{Line: 1, Column: 11, Offset: 11}, + }, + }, + Source: "// bar ", + }, + { + Token: Token{ + Type: TokenSpace, + SpaceOrError: Space{ + ContainsNewline: true, + }, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 12, Offset: 12}, + EndPos: ast.Position{Line: 2, Column: 0, Offset: 13}, + }, + }, + Source: "\n ", + }, + { + Token: Token{ + Type: TokenIdentifier, + Range: ast.Range{ + StartPos: ast.Position{Line: 2, Column: 1, Offset: 14}, + EndPos: ast.Position{Line: 2, Column: 3, Offset: 16}, + }, + }, + Source: "baz", + }, + { + Token: Token{ + Type: TokenEOF, + Range: ast.Range{ + StartPos: ast.Position{Line: 2, Column: 4, Offset: 17}, + EndPos: ast.Position{Line: 2, Column: 4, Offset: 17}, + }, + }, + }, + }, + ) + }) +} + +func TestRevert(t *testing.T) { + + t.Parallel() + + tokenStream := Lex([]byte("1 2 3"), nil) + + // Assert all tokens + + assert.Equal(t, + Token{ + Type: TokenDecimalIntegerLiteral, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + tokenStream.Next(), + ) + + assert.Equal(t, + Token{ + Type: TokenSpace, + SpaceOrError: Space{ + ContainsNewline: false, + }, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + EndPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + }, + }, + tokenStream.Next(), + ) + + twoCursor := tokenStream.Cursor() + + assert.Equal(t, + Token{ + Type: TokenDecimalIntegerLiteral, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + EndPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + }, + }, + tokenStream.Next(), + ) + + assert.Equal(t, + Token{ + Type: TokenSpace, + SpaceOrError: Space{ + ContainsNewline: false, + }, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 3, Offset: 3}, + EndPos: ast.Position{Line: 1, Column: 3, Offset: 3}, + }, + }, + tokenStream.Next(), + ) + + assert.Equal(t, + Token{ + Type: TokenDecimalIntegerLiteral, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + EndPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + }, + }, + tokenStream.Next(), + ) + + // Assert EOF keeps on being returned for Next() + // at the end of the stream + + assert.Equal(t, + Token{ + Type: TokenEOF, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 5, Offset: 5}, + EndPos: ast.Position{Line: 1, Column: 5, Offset: 5}, + }, + }, + tokenStream.Next(), + ) + + assert.Equal(t, + Token{ + Type: TokenEOF, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 5, Offset: 5}, + EndPos: ast.Position{Line: 1, Column: 5, Offset: 5}, + }, + }, + tokenStream.Next(), + ) + + // Revert back to token '2' + + tokenStream.Revert(twoCursor) + + // Re-assert tokens + + assert.Equal(t, + Token{ + Type: TokenDecimalIntegerLiteral, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + EndPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + }, + }, + tokenStream.Next(), + ) + + assert.Equal(t, + Token{ + Type: TokenSpace, + SpaceOrError: Space{ + ContainsNewline: false, + }, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 3, Offset: 3}, + EndPos: ast.Position{Line: 1, Column: 3, Offset: 3}, + }, + }, + tokenStream.Next(), + ) + + assert.Equal(t, + Token{ + Type: TokenDecimalIntegerLiteral, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + EndPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + }, + }, + tokenStream.Next(), + ) + + // Re-assert EOF keeps on being returned for Next() + // at the end of the stream + + assert.Equal(t, + Token{ + Type: TokenEOF, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 5, Offset: 5}, + EndPos: ast.Position{Line: 1, Column: 5, Offset: 5}, + }, + }, + tokenStream.Next(), + ) + + assert.Equal(t, + Token{ + Type: TokenEOF, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 5, Offset: 5}, + EndPos: ast.Position{Line: 1, Column: 5, Offset: 5}, + }, + }, + tokenStream.Next(), + ) + +} + +func TestEOFsAfterError(t *testing.T) { + + t.Parallel() + + tokenStream := Lex([]byte(`1 ''`), nil) + + // Assert all tokens + + assert.Equal(t, + Token{ + Type: TokenDecimalIntegerLiteral, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + tokenStream.Next(), + ) + + assert.Equal(t, + Token{ + Type: TokenSpace, + SpaceOrError: Space{ + ContainsNewline: false, + }, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + EndPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + }, + }, + tokenStream.Next(), + ) + + assert.Equal(t, + Token{ + Type: TokenError, + SpaceOrError: errors.New(`unrecognized character: U+0027 '''`), + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + EndPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + }, + }, + tokenStream.Next(), + ) + + // Assert EOFs keep on being returned for Next() + // at the end of the stream + + for i := 0; i < 10; i++ { + + require.Equal(t, + Token{ + Type: TokenEOF, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + EndPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + }, + }, + tokenStream.Next(), + ) + } +} + +func TestEOFsAfterEmptyInput(t *testing.T) { + + t.Parallel() + + tokenStream := Lex(nil, nil) + + // Assert EOFs keep on being returned for Next() + // at the end of the stream + + for i := 0; i < 10; i++ { + + require.Equal(t, + Token{ + Type: TokenEOF, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + tokenStream.Next(), + ) + } +} + +func TestLimit(t *testing.T) { + + t.Parallel() + + var b strings.Builder + for i := 0; i < 300000; i++ { + b.WriteString("x ") + } + + code := b.String() + + assert.PanicsWithValue(t, + TokenLimitReachedError{}, + func() { + _ = Lex([]byte(code), nil) + }, + ) +} diff --git a/runtime/old_parser/lexer/state.go b/runtime/old_parser/lexer/state.go new file mode 100644 index 0000000000..0978df343d --- /dev/null +++ b/runtime/old_parser/lexer/state.go @@ -0,0 +1,342 @@ +/* + * Cadence - The resource-oriented smart contract programming language + * + * Copyright Dapper Labs, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package lexer + +import ( + "fmt" + + "github.com/onflow/cadence/runtime/common" +) + +const keywordAs = "as" + +// stateFn uses the input lexer to read runes and emit tokens. +// +// It either returns nil when reaching end of file, +// or returns another stateFn for more scanning work. +type stateFn func(*lexer) stateFn + +// rootState returns a stateFn that scans the file and emits tokens until +// reaching the end of the file. +func rootState(l *lexer) stateFn { + for { + r := l.next() + switch r { + case EOF: + return nil + case '+': + l.emitType(TokenPlus) + case '-': + l.emitType(TokenMinus) + case '*': + l.emitType(TokenStar) + case '%': + l.emitType(TokenPercent) + case '(': + l.emitType(TokenParenOpen) + case ')': + l.emitType(TokenParenClose) + case '{': + l.emitType(TokenBraceOpen) + case '}': + l.emitType(TokenBraceClose) + case '[': + l.emitType(TokenBracketOpen) + case ']': + l.emitType(TokenBracketClose) + case ',': + l.emitType(TokenComma) + case ';': + l.emitType(TokenSemicolon) + case ':': + l.emitType(TokenColon) + case '.': + l.emitType(TokenDot) + case '=': + if l.acceptOne('=') { + l.emitType(TokenEqualEqual) + } else { + l.emitType(TokenEqual) + } + case '@': + l.emitType(TokenAt) + case '#': + l.emitType(TokenPragma) + case '&': + if l.acceptOne('&') { + l.emitType(TokenAmpersandAmpersand) + } else { + l.emitType(TokenAmpersand) + } + case '^': + l.emitType(TokenCaret) + case '|': + if l.acceptOne('|') { + l.emitType(TokenVerticalBarVerticalBar) + } else { + l.emitType(TokenVerticalBar) + } + case '>': + r = l.next() + switch r { + case '=': + l.emitType(TokenGreaterEqual) + default: + l.backupOne() + l.emitType(TokenGreater) + } + case '_': + return identifierState + case ' ', '\t', '\r': + return spaceState(false) + case '\n': + return spaceState(true) + case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': + return numberState + case '"': + return stringState + case '/': + r = l.next() + switch r { + case '/': + return lineCommentState + case '*': + l.emitType(TokenBlockCommentStart) + return blockCommentState(0) + default: + l.backupOne() + l.emitType(TokenSlash) + } + case '?': + r = l.next() + switch r { + case '?': + l.emitType(TokenDoubleQuestionMark) + case '.': + l.emitType(TokenQuestionMarkDot) + default: + l.backupOne() + l.emitType(TokenQuestionMark) + } + case '!': + if l.acceptOne('=') { + l.emitType(TokenNotEqual) + } else { + l.emitType(TokenExclamationMark) + } + case '<': + r = l.next() + switch r { + case '-': + r = l.next() + switch r { + case '!': + l.emitType(TokenLeftArrowExclamation) + case '>': + l.emitType(TokenSwap) + default: + l.backupOne() + l.emitType(TokenLeftArrow) + } + case '<': + l.emitType(TokenLessLess) + case '=': + l.emitType(TokenLessEqual) + default: + l.backupOne() + l.emitType(TokenLess) + } + default: + switch { + case r >= 'a' && r <= 'z' || + r >= 'A' && r <= 'Z': + + return identifierState + + default: + return l.error(fmt.Errorf("unrecognized character: %#U", r)) + } + } + } +} + +func (l *lexer) error(err error) stateFn { + l.emitError(err) + return nil +} + +// numberState returns a stateFn that scans the following runes as a number +// and emits a corresponding token +func numberState(l *lexer) stateFn { + // lookahead is already lexed. + // parse more, if any + r := l.current + if r == '0' { + r = l.next() + switch r { + case 'b': + l.scanBinaryRemainder() + if l.endOffset-l.startOffset <= 2 { + l.emitError(fmt.Errorf("missing digits")) + } + l.emitType(TokenBinaryIntegerLiteral) + + case 'o': + l.scanOctalRemainder() + if l.endOffset-l.startOffset <= 2 { + l.emitError(fmt.Errorf("missing digits")) + } + l.emitType(TokenOctalIntegerLiteral) + + case 'x': + l.scanHexadecimalRemainder() + if l.endOffset-l.startOffset <= 2 { + l.emitError(fmt.Errorf("missing digits")) + } + l.emitType(TokenHexadecimalIntegerLiteral) + + case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '_': + tokenType := l.scanDecimalOrFixedPointRemainder() + l.emitType(tokenType) + + case '.': + l.scanFixedPointRemainder() + l.emitType(TokenFixedPointNumberLiteral) + + case EOF: + l.backupOne() + l.emitType(TokenDecimalIntegerLiteral) + + default: + prefixChar := r + + if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') { + l.emitError(fmt.Errorf("invalid number literal prefix: %q", prefixChar)) + l.next() + + tokenType := l.scanDecimalOrFixedPointRemainder() + if tokenType == TokenDecimalIntegerLiteral { + tokenType = TokenUnknownBaseIntegerLiteral + } + l.emitType(tokenType) + } else { + l.backupOne() + l.emitType(TokenDecimalIntegerLiteral) + } + } + + } else { + tokenType := l.scanDecimalOrFixedPointRemainder() + l.emitType(tokenType) + } + + return rootState +} + +type Space struct { + ContainsNewline bool +} + +func spaceState(startIsNewline bool) stateFn { + return func(l *lexer) stateFn { + containsNewline := l.scanSpace() + containsNewline = containsNewline || startIsNewline + + common.UseMemory(l.memoryGauge, common.SpaceTokenMemoryUsage) + + l.emit( + TokenSpace, + Space{ + ContainsNewline: containsNewline, + }, + l.startPosition(), + true, + ) + return rootState + } +} + +func identifierState(l *lexer) stateFn { + l.scanIdentifier() + // https://github.com/golang/go/commit/69cd91a5981c49eaaa59b33196bdb5586c18d289 + if string(l.word()) == keywordAs { + r := l.next() + switch r { + case '?': + l.emitType(TokenAsQuestionMark) + return rootState + case '!': + l.emitType(TokenAsExclamationMark) + return rootState + default: + l.backupOne() + } + } + l.emitType(TokenIdentifier) + return rootState +} + +func stringState(l *lexer) stateFn { + l.scanString('"') + l.emitType(TokenString) + return rootState +} + +func lineCommentState(l *lexer) stateFn { + l.scanLineComment() + l.emitType(TokenLineComment) + return rootState +} + +func blockCommentState(nesting int) stateFn { + if nesting < 0 { + return rootState + } + + return func(l *lexer) stateFn { + r := l.next() + switch r { + case EOF: + return nil + case '/': + beforeSlashOffset := l.prevEndOffset + if l.acceptOne('*') { + starOffset := l.endOffset + l.endOffset = beforeSlashOffset + l.emitType(TokenBlockCommentContent) + l.endOffset = starOffset + l.emitType(TokenBlockCommentStart) + return blockCommentState(nesting + 1) + } + + case '*': + beforeStarOffset := l.prevEndOffset + if l.acceptOne('/') { + slashOffset := l.endOffset + l.endOffset = beforeStarOffset + l.emitType(TokenBlockCommentContent) + l.endOffset = slashOffset + l.emitType(TokenBlockCommentEnd) + return blockCommentState(nesting - 1) + } + } + + return blockCommentState(nesting) + } +} diff --git a/runtime/old_parser/lexer/token.go b/runtime/old_parser/lexer/token.go new file mode 100644 index 0000000000..71e37e5190 --- /dev/null +++ b/runtime/old_parser/lexer/token.go @@ -0,0 +1,39 @@ +/* + * Cadence - The resource-oriented smart contract programming language + * + * Copyright Dapper Labs, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package lexer + +import ( + "github.com/onflow/cadence/runtime/ast" +) + +type Token struct { + SpaceOrError any + ast.Range + Type TokenType +} + +func (t Token) Is(ty TokenType) bool { + return t.Type == ty +} + +func (t Token) Source(input []byte) []byte { + startOffset := t.StartPos.Offset + endOffset := t.EndPos.Offset + 1 + return input[startOffset:endOffset] +} diff --git a/runtime/old_parser/lexer/tokenstream.go b/runtime/old_parser/lexer/tokenstream.go new file mode 100644 index 0000000000..8e3f3e680a --- /dev/null +++ b/runtime/old_parser/lexer/tokenstream.go @@ -0,0 +1,29 @@ +/* + * Cadence - The resource-oriented smart contract programming language + * + * Copyright Dapper Labs, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package lexer + +type TokenStream interface { + // Next consumes and returns one Token. If there are no tokens remaining, it returns Token{TokenEOF} + Next() Token + Cursor() int + Revert(cursor int) + // Input returns the whole input as source code + Input() []byte + Reclaim() +} diff --git a/runtime/old_parser/lexer/tokentype.go b/runtime/old_parser/lexer/tokentype.go new file mode 100644 index 0000000000..b82a1fa408 --- /dev/null +++ b/runtime/old_parser/lexer/tokentype.go @@ -0,0 +1,222 @@ +/* + * Cadence - The resource-oriented smart contract programming language + * + * Copyright Dapper Labs, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package lexer + +import ( + "github.com/onflow/cadence/runtime/errors" +) + +type TokenType uint8 + +const EOF rune = -1 + +const ( + TokenError TokenType = iota + TokenEOF + TokenSpace + TokenBinaryIntegerLiteral + TokenOctalIntegerLiteral + TokenDecimalIntegerLiteral + TokenHexadecimalIntegerLiteral + TokenUnknownBaseIntegerLiteral + TokenFixedPointNumberLiteral + TokenIdentifier + TokenString + TokenPlus + TokenMinus + TokenStar + TokenSlash + TokenPercent + TokenDoubleQuestionMark + TokenParenOpen + TokenParenClose + TokenBraceOpen + TokenBraceClose + TokenBracketOpen + TokenBracketClose + TokenQuestionMark + TokenQuestionMarkDot + TokenComma + TokenColon + TokenDot + TokenSemicolon + TokenLeftArrow + TokenLeftArrowExclamation + TokenSwap + TokenLess + TokenLessEqual + TokenLessLess + TokenGreater + TokenGreaterEqual + TokenEqual + TokenEqualEqual + TokenExclamationMark + TokenNotEqual + TokenBlockCommentStart + TokenBlockCommentEnd + TokenBlockCommentContent + TokenLineComment + TokenAmpersand + TokenAmpersandAmpersand + TokenCaret + TokenVerticalBar + TokenVerticalBarVerticalBar + TokenAt + TokenAsExclamationMark + TokenAsQuestionMark + TokenPragma + // NOTE: not an actual token, must be last item + TokenMax +) + +func init() { + // ensure all tokens have its string format + for t := TokenType(0); t < TokenMax; t++ { + _ = t.String() + } +} + +func (t TokenType) String() string { + switch t { + case TokenError: + return "error" + case TokenEOF: + return "EOF" + case TokenSpace: + return "space" + case TokenBinaryIntegerLiteral: + return "binary integer" + case TokenOctalIntegerLiteral: + return "octal integer" + case TokenDecimalIntegerLiteral: + return "decimal integer" + case TokenHexadecimalIntegerLiteral: + return "hexadecimal integer" + case TokenFixedPointNumberLiteral: + return "fixed-point number" + case TokenUnknownBaseIntegerLiteral: + return "integer with unknown base" + case TokenIdentifier: + return "identifier" + case TokenString: + return "string" + case TokenPlus: + return `'+'` + case TokenMinus: + return `'-'` + case TokenStar: + return `'*'` + case TokenSlash: + return `'/'` + case TokenPercent: + return `'%'` + case TokenDoubleQuestionMark: + return `'??'` + case TokenParenOpen: + return `'('` + case TokenParenClose: + return `')'` + case TokenBraceOpen: + return `'{'` + case TokenBraceClose: + return `'}'` + case TokenBracketOpen: + return `'['` + case TokenBracketClose: + return `']'` + case TokenQuestionMark: + return `'?'` + case TokenQuestionMarkDot: + return `'?.'` + case TokenComma: + return `','` + case TokenColon: + return `':'` + case TokenDot: + return `'.'` + case TokenSemicolon: + return `';'` + case TokenLeftArrow: + return `'<-'` + case TokenLeftArrowExclamation: + return `'<-!'` + case TokenSwap: + return `'<->'` + case TokenLess: + return `'<'` + case TokenLessEqual: + return `'<='` + case TokenLessLess: + return `'<<'` + case TokenGreater: + return `'>'` + case TokenGreaterEqual: + return `'>='` + case TokenEqual: + return `'='` + case TokenEqualEqual: + return `'=='` + case TokenExclamationMark: + return `'!'` + case TokenNotEqual: + return `'!='` + case TokenBlockCommentStart: + return `'/*'` + case TokenBlockCommentContent: + return "block comment" + case TokenLineComment: + return "line comment" + case TokenBlockCommentEnd: + return `'*/'` + case TokenAmpersand: + return `'&'` + case TokenAmpersandAmpersand: + return `'&&'` + case TokenCaret: + return `'^'` + case TokenVerticalBar: + return `'|'` + case TokenVerticalBarVerticalBar: + return `'||'` + case TokenAt: + return `'@'` + case TokenAsExclamationMark: + return `'as!'` + case TokenAsQuestionMark: + return `'as?'` + case TokenPragma: + return `'#'` + default: + panic(errors.NewUnreachableError()) + } +} + +func (t TokenType) IsIntegerLiteral() bool { + switch t { + case TokenBinaryIntegerLiteral, + TokenOctalIntegerLiteral, + TokenDecimalIntegerLiteral, + TokenHexadecimalIntegerLiteral, + TokenUnknownBaseIntegerLiteral: + return true + + default: + return false + } +} diff --git a/runtime/old_parser/parser.go b/runtime/old_parser/parser.go new file mode 100644 index 0000000000..c7af7e7796 --- /dev/null +++ b/runtime/old_parser/parser.go @@ -0,0 +1,693 @@ +/* + * Cadence - The resource-oriented smart contract programming language + * + * Copyright Dapper Labs, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package old_parser + +import ( + "bytes" + "os" + "strings" + + "github.com/onflow/cadence/runtime/ast" + "github.com/onflow/cadence/runtime/common" + "github.com/onflow/cadence/runtime/errors" + "github.com/onflow/cadence/runtime/parser/lexer" +) + +// expressionDepthLimit is the limit of how deeply nested an expression can get +const expressionDepthLimit = 1 << 4 + +// typeDepthLimit is the limit of how deeply nested a type can get +const typeDepthLimit = 1 << 4 + +// lowestBindingPower is the lowest binding power. +// The binding power controls operator precedence: +// the higher the value, the tighter a token binds to the tokens that follow. + +const lowestBindingPower = 0 + +type Config struct { + // StaticModifierEnabled determines if the static modifier is enabled + StaticModifierEnabled bool + // NativeModifierEnabled determines if the native modifier is enabled + NativeModifierEnabled bool + // TypeParametersEnabled determines if type parameters are enabled + TypeParametersEnabled bool +} + +type parser struct { + // tokens is a stream of tokens from the lexer + tokens lexer.TokenStream + // memoryGauge is used for metering memory usage + memoryGauge common.MemoryGauge + // errors are the parsing errors encountered during parsing + errors []error + // backtrackingCursorStack is the stack of lexer cursors used when backtracking + backtrackingCursorStack []int + // bufferedErrorsStack is the stack of parsing errors encountered during buffering + bufferedErrorsStack [][]error + // current is the current token being parsed + current lexer.Token + // localReplayedTokensCount is the number of replayed tokens since starting the top-most ambiguity. + // Reset when the top-most ambiguity starts and ends. This keeps errors local + localReplayedTokensCount uint + // globalReplayedTokensCount is the number of replayed tokens since starting the parse. + // It is never reset + globalReplayedTokensCount uint + // ambiguityLevel is the current level of ambiguity (nesting) + ambiguityLevel int + // expressionDepth is the depth of the currently parsed expression (if >0) + expressionDepth int + // typeDepth is the depth of the type (if >0) + typeDepth int + // config enables certain features + config Config +} + +// Parse creates a lexer to scan the given input string, +// and uses the given `parse` function to parse tokens into a result. +// +// It can be composed with different parse functions to parse the input string into different results. +// See "ParseExpression", "ParseStatements" as examples. +func Parse[T any]( + memoryGauge common.MemoryGauge, + input []byte, + parse func(*parser) (T, error), + config Config, +) (result T, errors []error) { + // create a lexer, which turns the input string into tokens + tokens := lexer.Lex(input, memoryGauge) + defer tokens.Reclaim() + return ParseTokenStream( + memoryGauge, + tokens, + parse, + config, + ) +} + +func ParseTokenStream[T any]( + memoryGauge common.MemoryGauge, + tokens lexer.TokenStream, + parse func(*parser) (T, error), + config Config, +) ( + result T, + errs []error, +) { + p := &parser{ + config: config, + tokens: tokens, + memoryGauge: memoryGauge, + } + + defer func() { + if r := recover(); r != nil { + switch r := r.(type) { + case ParseError: + // Report parser errors. + p.report(r) + + // Do not treat non-parser errors as syntax errors. + case errors.InternalError, errors.UserError: + // Also do not wrap non-parser errors, that are already + // known cadence errors. i.e: internal errors / user errors. + // e.g: `errors.MemoryError` + panic(r) + case error: + // Any other error/panic is an internal error. + // Thus, wrap with an UnexpectedError to mark it as an internal error + // and propagate up the call stack. + panic(errors.NewUnexpectedErrorFromCause(r)) + default: + panic(errors.NewUnexpectedError("parser: %v", r)) + } + + var zero T + result = zero + errs = p.errors + } + + for _, bufferedErrors := range p.bufferedErrorsStack { + errs = append(errs, bufferedErrors...) + } + }() + + startPos := ast.NewPosition( + p.memoryGauge, + 0, + 1, + 0, + ) + + p.current = lexer.Token{ + Type: lexer.TokenEOF, + Range: ast.NewRange( + p.memoryGauge, + startPos, + startPos, + ), + } + + // Get the initial token + p.next() + + result, err := parse(p) + if err != nil { + p.report(err) + var zero T + return zero, p.errors + } + + p.skipSpaceAndComments() + + if !p.current.Is(lexer.TokenEOF) { + p.reportSyntaxError("unexpected token: %s", p.current.Type) + } + + return result, p.errors +} + +func (p *parser) syntaxError(message string, params ...any) error { + return NewSyntaxError(p.current.StartPos, message, params...) +} + +func (p *parser) reportSyntaxError(message string, params ...any) { + err := p.syntaxError(message, params...) + p.report(err) +} + +func (p *parser) report(errs ...error) { + for _, err := range errs { + + // Only `ParserError`s must be reported. + // If the reported error is not a parse error, then it's an internal error (go runtime errors), + // or a fatal error (e.g: MemoryError) + // Hence, terminate parsing. + parseError, ok := err.(ParseError) + if !ok { + panic(err) + } + + // Add the errors to the buffered errors if buffering, + // or the final errors if not + + bufferedErrorsDepth := len(p.bufferedErrorsStack) + if bufferedErrorsDepth > 0 { + bufferedErrorsIndex := bufferedErrorsDepth - 1 + p.bufferedErrorsStack[bufferedErrorsIndex] = append( + p.bufferedErrorsStack[bufferedErrorsIndex], + parseError, + ) + } else { + p.errors = append(p.errors, parseError) + } + } +} + +// next reads the next token and marks it as the "current" token. +// The next token could either be read from the lexer or from +// the buffer. +// Tokens are buffered when syntax ambiguity is involved. +func (p *parser) next() { + + for { + token := p.tokens.Next() + + if token.Is(lexer.TokenError) { + // Report error token as error, skip. + err, ok := token.SpaceOrError.(error) + // we just checked that this is an error token + if !ok { + panic(errors.NewUnreachableError()) + } + parseError, ok := err.(ParseError) + if !ok { + parseError = NewSyntaxError( + token.StartPos, + err.Error(), + ) + } + p.report(parseError) + continue + } + + p.current = token + + return + } +} + +// nextSemanticToken advances past the current token to the next semantic token. +// It skips whitespace, including newlines, and comments +func (p *parser) nextSemanticToken() { + p.next() + p.skipSpaceAndComments() +} + +func (p *parser) mustOne(tokenType lexer.TokenType) (lexer.Token, error) { + t := p.current + if !t.Is(tokenType) { + return lexer.Token{}, p.syntaxError("expected token %s", tokenType) + } + p.next() + return t, nil +} + +func (p *parser) tokenSource(token lexer.Token) []byte { + input := p.tokens.Input() + return token.Source(input) +} + +func (p *parser) currentTokenSource() []byte { + return p.tokenSource(p.current) +} + +func (p *parser) isToken(token lexer.Token, tokenType lexer.TokenType, expected string) bool { + if !token.Is(tokenType) { + return false + } + + actual := p.tokenSource(token) + return string(actual) == expected +} + +func (p *parser) mustToken(tokenType lexer.TokenType, string string) (lexer.Token, error) { + t := p.current + if !p.isToken(t, tokenType, string) { + return lexer.Token{}, p.syntaxError("expected token %s with string value %s", tokenType, string) + } + p.next() + return t, nil +} + +func (p *parser) startBuffering() { + // Push the lexer's previous cursor to the stack. + // When start buffering is called, the lexer has already advanced to the next token + p.backtrackingCursorStack = append(p.backtrackingCursorStack, p.tokens.Cursor()-1) + + // Push an empty slice of errors to the stack + p.bufferedErrorsStack = append(p.bufferedErrorsStack, nil) +} + +func (p *parser) acceptBuffered() { + // Pop the last backtracking cursor from the stack + // and ignore it + + lastIndex := len(p.backtrackingCursorStack) - 1 + p.backtrackingCursorStack = p.backtrackingCursorStack[:lastIndex] + + // Pop the last buffered errors from the stack. + // + // The element type is a slice (reference type), + // so we need to replace the slice with nil explicitly + // to free the memory. + // The slice's underlying storage would otherwise + // keep a reference to it and prevent it from being garbage collected. + + lastIndex = len(p.bufferedErrorsStack) - 1 + bufferedErrors := p.bufferedErrorsStack[lastIndex] + p.bufferedErrorsStack[lastIndex] = nil + p.bufferedErrorsStack = p.bufferedErrorsStack[:lastIndex] + + // Apply the accepted buffered errors to the last errors on the buffered errors stack, + // or the final errors, if we reached the bottom of the stack + // (i.e. this acceptance disables buffering) + + if len(p.bufferedErrorsStack) > 0 { + p.bufferedErrorsStack[lastIndex-1] = append( + p.bufferedErrorsStack[lastIndex-1], + bufferedErrors..., + ) + } else { + p.errors = append( + p.errors, + bufferedErrors..., + ) + } +} + +// localTokenReplayCountLimit is a sensible limit for how many tokens may be replayed +// until the top-most ambiguity ends. +const localTokenReplayCountLimit = 1 << 6 + +// globalTokenReplayCountLimit is a sensible limit for how many tokens may be replayed +// during a parse +const globalTokenReplayCountLimit = 1 << 10 + +func (p *parser) checkReplayCount(total, additional, limit uint, kind string) (uint, error) { + newTotal := total + additional + // Check for overflow (uint) and for exceeding the limit + if newTotal < total || newTotal > limit { + return newTotal, p.syntaxError("program too ambiguous, %s replay limit of %d tokens exceeded", kind, limit) + } + return newTotal, nil +} + +func (p *parser) replayBuffered() error { + + cursor := p.tokens.Cursor() + + // Pop the last backtracking cursor from the stack + // and revert the lexer back to it + + lastIndex := len(p.backtrackingCursorStack) - 1 + backtrackCursor := p.backtrackingCursorStack[lastIndex] + + replayedCount := uint(cursor - backtrackCursor) + + var err error + + p.localReplayedTokensCount, err = p.checkReplayCount( + p.localReplayedTokensCount, + replayedCount, + localTokenReplayCountLimit, + "local", + ) + if err != nil { + return err + } + + p.globalReplayedTokensCount, err = p.checkReplayCount( + p.globalReplayedTokensCount, + replayedCount, + globalTokenReplayCountLimit, + "global", + ) + if err != nil { + return err + } + + p.tokens.Revert(backtrackCursor) + p.next() + p.backtrackingCursorStack = p.backtrackingCursorStack[:lastIndex] + + // Pop the last buffered errors from the stack + // and ignore them + + lastIndex = len(p.bufferedErrorsStack) - 1 + p.bufferedErrorsStack[lastIndex] = nil + p.bufferedErrorsStack = p.bufferedErrorsStack[:lastIndex] + + return nil +} + +type triviaOptions struct { + skipNewlines bool + parseDocStrings bool +} + +// skipSpaceAndComments skips whitespace, including newlines, and comments +func (p *parser) skipSpaceAndComments() (containsNewline bool) { + containsNewline, _ = p.parseTrivia(triviaOptions{ + skipNewlines: true, + }) + return +} + +var blockCommentDocStringPrefix = []byte("/**") +var lineCommentDocStringPrefix = []byte("///") + +func (p *parser) parseTrivia(options triviaOptions) (containsNewline bool, docString string) { + var docStringBuilder strings.Builder + defer func() { + if options.parseDocStrings { + docString = docStringBuilder.String() + } + }() + + var atEnd, insideLineDocString bool + + for !atEnd { + switch p.current.Type { + case lexer.TokenSpace: + space, ok := p.current.SpaceOrError.(lexer.Space) + // we just checked that this is a space + if !ok { + panic(errors.NewUnreachableError()) + } + + if space.ContainsNewline { + containsNewline = true + } + + if containsNewline && !options.skipNewlines { + return + } + + p.next() + + case lexer.TokenBlockCommentStart: + commentStartOffset := p.current.StartPos.Offset + endToken, ok := p.parseBlockComment() + + if ok && options.parseDocStrings { + commentEndOffset := endToken.EndPos.Offset + + contentWithPrefix := p.tokens.Input()[commentStartOffset : commentEndOffset-1] + + insideLineDocString = false + docStringBuilder.Reset() + if bytes.HasPrefix(contentWithPrefix, blockCommentDocStringPrefix) { + // Strip prefix (`/**`) + docStringBuilder.Write(contentWithPrefix[len(blockCommentDocStringPrefix):]) + } + } + + case lexer.TokenLineComment: + if options.parseDocStrings { + comment := p.currentTokenSource() + if bytes.HasPrefix(comment, lineCommentDocStringPrefix) { + if insideLineDocString { + docStringBuilder.WriteByte('\n') + } else { + insideLineDocString = true + docStringBuilder.Reset() + } + // Strip prefix + docStringBuilder.Write(comment[len(lineCommentDocStringPrefix):]) + } else { + insideLineDocString = false + docStringBuilder.Reset() + } + } + + p.next() + + default: + atEnd = true + } + } + return +} + +func (p *parser) mustIdentifier() (ast.Identifier, error) { + identifier, err := p.mustOne(lexer.TokenIdentifier) + if err != nil { + return ast.Identifier{}, err + } + + return p.tokenToIdentifier(identifier), err +} + +func (p *parser) tokenToIdentifier(token lexer.Token) ast.Identifier { + return ast.NewIdentifier( + p.memoryGauge, + string(p.tokenSource(token)), + token.StartPos, + ) +} + +func (p *parser) startAmbiguity() { + if p.ambiguityLevel == 0 { + p.localReplayedTokensCount = 0 + } + p.ambiguityLevel++ +} + +func (p *parser) endAmbiguity() { + p.ambiguityLevel-- + if p.ambiguityLevel == 0 { + p.localReplayedTokensCount = 0 + } +} + +func ParseExpression( + memoryGauge common.MemoryGauge, + input []byte, + config Config, +) ( + expression ast.Expression, + errs []error, +) { + return Parse( + memoryGauge, + input, + func(p *parser) (ast.Expression, error) { + return parseExpression(p, lowestBindingPower) + }, + config, + ) +} + +func ParseStatements( + memoryGauge common.MemoryGauge, + input []byte, + config Config, +) ( + statements []ast.Statement, + errs []error, +) { + return Parse( + memoryGauge, + input, + func(p *parser) ([]ast.Statement, error) { + return parseStatements(p, nil) + }, + config, + ) +} + +func ParseStatementsFromTokenStream( + memoryGauge common.MemoryGauge, + tokens lexer.TokenStream, + config Config, +) ( + statements []ast.Statement, + errs []error, +) { + return ParseTokenStream( + memoryGauge, + tokens, + func(p *parser) ([]ast.Statement, error) { + return parseStatements(p, nil) + }, + config, + ) +} + +func ParseType(memoryGauge common.MemoryGauge, input []byte, config Config) (ty ast.Type, errs []error) { + return Parse( + memoryGauge, + input, + func(p *parser) (ast.Type, error) { + return parseType(p, lowestBindingPower) + }, + config, + ) +} + +func ParseDeclarations( + memoryGauge common.MemoryGauge, + input []byte, + config Config, +) ( + declarations []ast.Declaration, + errs []error, +) { + return Parse( + memoryGauge, + input, + func(p *parser) ([]ast.Declaration, error) { + return parseDeclarations(p, lexer.TokenEOF) + }, + config, + ) +} + +func ParseArgumentList( + memoryGauge common.MemoryGauge, + input []byte, + config Config, +) ( + arguments ast.Arguments, + errs []error, +) { + return Parse( + memoryGauge, + input, + func(p *parser) (ast.Arguments, error) { + p.skipSpaceAndComments() + + _, err := p.mustOne(lexer.TokenParenOpen) + if err != nil { + return nil, err + } + + arguments, _, err := parseArgumentListRemainder(p) + return arguments, err + }, + config, + ) +} + +func ParseProgram(memoryGauge common.MemoryGauge, code []byte, config Config) (program *ast.Program, err error) { + tokens := lexer.Lex(code, memoryGauge) + defer tokens.Reclaim() + return ParseProgramFromTokenStream(memoryGauge, tokens, config) +} + +func ParseProgramFromTokenStream( + memoryGauge common.MemoryGauge, + input lexer.TokenStream, + config Config, +) ( + program *ast.Program, + err error, +) { + declarations, errs := ParseTokenStream( + memoryGauge, + input, + func(p *parser) ([]ast.Declaration, error) { + return parseDeclarations(p, lexer.TokenEOF) + }, + config, + ) + if len(errs) > 0 { + err = Error{ + Code: input.Input(), + Errors: errs, + } + } + + program = ast.NewProgram(memoryGauge, declarations) + + return +} + +func ParseProgramFromFile( + memoryGauge common.MemoryGauge, + filename string, + config Config, +) ( + program *ast.Program, + code []byte, + err error, +) { + var data []byte + data, err = os.ReadFile(filename) + if err != nil { + return nil, nil, err + } + + program, err = ParseProgram(memoryGauge, data, config) + if err != nil { + return nil, code, err + } + return program, code, nil +} diff --git a/runtime/old_parser/parser_test.go b/runtime/old_parser/parser_test.go new file mode 100644 index 0000000000..4537bf6118 --- /dev/null +++ b/runtime/old_parser/parser_test.go @@ -0,0 +1,1013 @@ +/* + * Cadence - The resource-oriented smart contract programming language + * + * Copyright Dapper Labs, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package old_parser + +import ( + "fmt" + "math/big" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "go.uber.org/goleak" + + "github.com/onflow/cadence/runtime/ast" + "github.com/onflow/cadence/runtime/common" + "github.com/onflow/cadence/runtime/errors" + "github.com/onflow/cadence/runtime/parser/lexer" + "github.com/onflow/cadence/runtime/tests/utils" +) + +func TestMain(m *testing.M) { + goleak.VerifyTestMain(m) +} + +type testTokenStream struct { + tokens []lexer.Token + input []byte + cursor int +} + +var _ lexer.TokenStream = &testTokenStream{} + +func (t *testTokenStream) Next() lexer.Token { + if t.cursor >= len(t.tokens) { + + // At the end of the token stream, + // emit a synthetic EOF token + + return lexer.Token{ + Type: lexer.TokenEOF, + } + + } + token := t.tokens[t.cursor] + t.cursor++ + return token +} + +func (t *testTokenStream) Cursor() int { + return t.cursor +} + +func (t *testTokenStream) Revert(cursor int) { + t.cursor = cursor +} + +func (t *testTokenStream) Input() []byte { + return t.input +} + +func (*testTokenStream) Reclaim() { + // NO-OP +} + +func testParseStatements(s string) ([]ast.Statement, []error) { + return ParseStatements(nil, []byte(s), Config{}) +} + +func testParseDeclarations(s string) ([]ast.Declaration, []error) { + return ParseDeclarations(nil, []byte(s), Config{}) +} + +func testParseProgram(s string) (*ast.Program, error) { + return ParseProgram(nil, []byte(s), Config{}) +} + +func testParseExpression(s string) (ast.Expression, []error) { + return ParseExpression(nil, []byte(s), Config{}) +} + +func testParseArgumentList(s string) (ast.Arguments, []error) { + return ParseArgumentList(nil, []byte(s), Config{}) +} + +func testParseType(s string) (ast.Type, []error) { + return ParseType(nil, []byte(s), Config{}) +} + +func TestParseInvalid(t *testing.T) { + t.Parallel() + + type test struct { + msg string + code string + } + + unexpectedToken := "Parsing failed:\nerror: unexpected token: identifier" + unexpectedEndOfProgram := "Parsing failed:\nerror: unexpected end of program" + missingTypeAnnotation := "Parsing failed:\nerror: missing type annotation after comma" + + for _, test := range []test{ + {unexpectedToken, "X"}, + {unexpectedToken, "paste your code in here"}, + {unexpectedEndOfProgram, "# a ( b > c > d > e > f > g > h > i > j > k > l > m > n > o > p > q > r >"}, + {missingTypeAnnotation, "#0x0<{},>()"}, + } { + t.Run(test.code, func(t *testing.T) { + _, err := testParseProgram(test.code) + require.ErrorContains(t, err, test.msg) + }) + } +} + +func TestParseBuffering(t *testing.T) { + + t.Parallel() + + t.Run("buffer and accept, valid", func(t *testing.T) { + + t.Parallel() + + _, errs := Parse( + nil, + []byte("a b c d"), + func(p *parser) (struct{}, error) { + _, err := p.mustToken(lexer.TokenIdentifier, "a") + if err != nil { + return struct{}{}, err + } + + _, err = p.mustOne(lexer.TokenSpace) + if err != nil { + return struct{}{}, err + } + + p.startBuffering() + + _, err = p.mustToken(lexer.TokenIdentifier, "b") + if err != nil { + return struct{}{}, err + } + + _, err = p.mustOne(lexer.TokenSpace) + if err != nil { + return struct{}{}, err + } + + _, err = p.mustToken(lexer.TokenIdentifier, "c") + if err != nil { + return struct{}{}, err + } + + p.acceptBuffered() + + _, err = p.mustOne(lexer.TokenSpace) + if err != nil { + return struct{}{}, err + } + + _, err = p.mustToken(lexer.TokenIdentifier, "d") + if err != nil { + return struct{}{}, err + } + + return struct{}{}, nil + }, + Config{}, + ) + + assert.Empty(t, errs) + }) + + t.Run("buffer and accept, invalid", func(t *testing.T) { + + t.Parallel() + + _, errs := Parse( + nil, + []byte("a b x d"), + func(p *parser) (struct{}, error) { + _, err := p.mustToken(lexer.TokenIdentifier, "a") + if err != nil { + return struct{}{}, err + } + + _, err = p.mustOne(lexer.TokenSpace) + if err != nil { + return struct{}{}, err + } + + p.startBuffering() + + _, err = p.mustToken(lexer.TokenIdentifier, "b") + if err != nil { + return struct{}{}, err + } + + _, err = p.mustOne(lexer.TokenSpace) + if err != nil { + return struct{}{}, err + } + + _, err = p.mustToken(lexer.TokenIdentifier, "c") + if err != nil { + return struct{}{}, err + } + + p.acceptBuffered() + + _, err = p.mustOne(lexer.TokenSpace) + if err != nil { + return struct{}{}, err + } + + _, err = p.mustToken(lexer.TokenIdentifier, "d") + if err != nil { + return struct{}{}, err + } + + return struct{}{}, nil + }, + Config{}, + ) + + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "expected token identifier with string value c", + Pos: ast.Position{Offset: 4, Line: 1, Column: 4}, + }, + }, + errs, + ) + }) + + t.Run("buffer and replay, valid", func(t *testing.T) { + + t.Parallel() + + _, errs := Parse( + nil, + []byte("a b c d"), + func(p *parser) (struct{}, error) { + _, err := p.mustToken(lexer.TokenIdentifier, "a") + if err != nil { + return struct{}{}, err + } + _, err = p.mustOne(lexer.TokenSpace) + if err != nil { + return struct{}{}, err + } + + p.startBuffering() + + _, err = p.mustToken(lexer.TokenIdentifier, "b") + if err != nil { + return struct{}{}, err + } + _, err = p.mustOne(lexer.TokenSpace) + if err != nil { + return struct{}{}, err + } + _, err = p.mustToken(lexer.TokenIdentifier, "c") + if err != nil { + return struct{}{}, err + } + + err = p.replayBuffered() + if err != nil { + return struct{}{}, err + } + + _, err = p.mustToken(lexer.TokenIdentifier, "b") + if err != nil { + return struct{}{}, err + } + _, err = p.mustOne(lexer.TokenSpace) + if err != nil { + return struct{}{}, err + } + _, err = p.mustToken(lexer.TokenIdentifier, "c") + if err != nil { + return struct{}{}, err + } + _, err = p.mustOne(lexer.TokenSpace) + if err != nil { + return struct{}{}, err + } + _, err = p.mustToken(lexer.TokenIdentifier, "d") + if err != nil { + return struct{}{}, err + } + + return struct{}{}, nil + }, + Config{}, + ) + + assert.Empty(t, errs) + }) + + t.Run("buffer and replay, invalid first", func(t *testing.T) { + + t.Parallel() + + _, errs := Parse( + nil, + []byte("a b c d"), + func(p *parser) (struct{}, error) { + _, err := p.mustToken(lexer.TokenIdentifier, "a") + if err != nil { + return struct{}{}, err + } + _, err = p.mustOne(lexer.TokenSpace) + if err != nil { + return struct{}{}, err + } + + p.startBuffering() + + firstSucceeded := false + firstFailed := false + + // Ignore error + func() { + var bufferingError error + + defer func() { + if r := recover(); r != nil || bufferingError != nil { + firstFailed = true + } + }() + + _, bufferingError = p.mustToken(lexer.TokenIdentifier, "x") + if bufferingError != nil { + return + } + _, bufferingError = p.mustOne(lexer.TokenSpace) + if bufferingError != nil { + return + } + _, bufferingError = p.mustToken(lexer.TokenIdentifier, "c") + if bufferingError != nil { + return + } + + firstSucceeded = true + }() + + assert.True(t, firstFailed) + assert.False(t, firstSucceeded) + + err = p.replayBuffered() + if err != nil { + return struct{}{}, err + } + + _, err = p.mustToken(lexer.TokenIdentifier, "b") + if err != nil { + return struct{}{}, err + } + _, err = p.mustOne(lexer.TokenSpace) + if err != nil { + return struct{}{}, err + } + _, err = p.mustToken(lexer.TokenIdentifier, "c") + if err != nil { + return struct{}{}, err + } + _, err = p.mustOne(lexer.TokenSpace) + if err != nil { + return struct{}{}, err + } + _, err = p.mustToken(lexer.TokenIdentifier, "d") + if err != nil { + return struct{}{}, err + } + + return struct{}{}, nil + }, + Config{}, + ) + + assert.Empty(t, errs) + }) + + t.Run("buffer and replay, invalid first and invalid second", func(t *testing.T) { + + t.Parallel() + + _, errs := Parse( + nil, + []byte("a b c x"), + func(p *parser) (any, error) { + _, err := p.mustToken(lexer.TokenIdentifier, "a") + if err != nil { + return nil, err + } + _, err = p.mustOne(lexer.TokenSpace) + if err != nil { + return nil, err + } + + p.startBuffering() + + firstSucceeded := false + firstFailed := false + + func() { + var bufferingError error + + defer func() { + if r := recover(); r != nil || bufferingError != nil { + firstFailed = true + } + }() + + _, bufferingError = p.mustToken(lexer.TokenIdentifier, "x") + if bufferingError != nil { + return + } + _, bufferingError = p.mustOne(lexer.TokenSpace) + if bufferingError != nil { + return + } + _, bufferingError = p.mustToken(lexer.TokenIdentifier, "c") + if bufferingError != nil { + return + } + + firstSucceeded = true + }() + + assert.True(t, firstFailed) + assert.False(t, firstSucceeded) + + err = p.replayBuffered() + if err != nil { + return nil, err + } + + _, err = p.mustToken(lexer.TokenIdentifier, "b") + if err != nil { + return nil, err + } + _, err = p.mustOne(lexer.TokenSpace) + if err != nil { + return nil, err + } + _, err = p.mustToken(lexer.TokenIdentifier, "c") + if err != nil { + return nil, err + } + _, err = p.mustOne(lexer.TokenSpace) + if err != nil { + return nil, err + } + _, err = p.mustToken(lexer.TokenIdentifier, "d") + if err != nil { + return nil, err + } + + return nil, nil + }, + Config{}, + ) + + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "expected token identifier with string value d", + Pos: ast.Position{Offset: 6, Line: 1, Column: 6}, + }, + }, + errs, + ) + }) + + t.Run("nested buffering, invalid", func(t *testing.T) { + + t.Parallel() + + const code = ` + fun main() { + assert(isneg(x:-1.0)) + assert(!isneg(x:-0.0/0.0)) + } + + fun isneg(x: SignedFixedPoint): Bool { /* I kinda forget what this is all about */ + return x /* but we probably need to figure it out */ + < /* ************/((TODO?{/*))************ *// + -x /* maybe it says NaNs are not negative? */ + } + ` + _, err := testParseProgram(code) + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "expected token identifier", + Pos: ast.Position{Offset: 399, Line: 9, Column: 95}, + }, + }, + err.(Error).Errors, + ) + }) + + t.Run("nested buffering, invalid; apparent invocation elision", func(t *testing.T) { + + t.Parallel() + + const code = ` + fun main() { + fun abs(_:Int):Int { return _ > 0 ? _ : -_ } + let sanity = 0 < /*****/((TODO?{/*****// + abs(-1) + assert(sanity) + } + ` + _, err := testParseProgram(code) + + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "expected token identifier", + Pos: ast.Position{Offset: 146, Line: 4, Column: 63}, + }, + }, + err.(Error).Errors, + ) + }) + + t.Run("nested buffering, valid; accept,accept,replay", func(t *testing.T) { + + t.Parallel() + + src := ` + pub struct interface Y {} + pub struct X : Y {} + pub fun main():String { + fun f(a:Bool, _:String):String { return _; } + let S = 1 + if false { + let Type_X_Y__qp_identifier = + Type().identifier; // parses fine + return f(a:S().identifier) // should also parse fine + } + }` + + _, err := testParseProgram(src) + assert.NoError(t, err) + }) + + t.Run("nested buffering, valid; overlapped", func(t *testing.T) { + + t.Parallel() + + src := ` + transaction { } + pub fun main():String { + let A = 1 + let B = 2 + let C = 3 + let D = 4 + fun g(a:Bool, _:Bool):String { return _ ? "y" : "n" } + return g(a:A>(5))) + }` + + _, err := testParseProgram(src) + assert.NoError(t, err) + }) + +} + +func TestParseEOF(t *testing.T) { + + t.Parallel() + + _, errs := Parse( + nil, + []byte("a b"), + func(p *parser) (struct{}, error) { + _, err := p.mustToken(lexer.TokenIdentifier, "a") + if err != nil { + return struct{}{}, err + } + p.skipSpaceAndComments() + _, err = p.mustToken(lexer.TokenIdentifier, "b") + if err != nil { + return struct{}{}, err + } + + p.next() + + assert.Equal(t, + lexer.Token{ + Type: lexer.TokenEOF, + Range: ast.Range{ + StartPos: ast.Position{Offset: 3, Line: 1, Column: 3}, + EndPos: ast.Position{Offset: 3, Line: 1, Column: 3}, + }, + }, + p.current, + ) + + p.next() + + assert.Equal(t, + lexer.Token{ + Type: lexer.TokenEOF, + Range: ast.Range{ + StartPos: ast.Position{Offset: 3, Line: 1, Column: 3}, + EndPos: ast.Position{Offset: 3, Line: 1, Column: 3}, + }, + }, + p.current, + ) + + return struct{}{}, nil + }, + Config{}, + ) + + assert.Empty(t, errs) +} + +func TestParseNames(t *testing.T) { + + t.Parallel() + + names := map[string]bool{ + // Valid: title-case + // + "PersonID": true, + + // Valid: with underscore + // + "token_name": true, + + // Valid: leading underscore and characters + // + "_balance": true, + + // Valid: leading underscore and numbers + "_8264": true, + + // Valid: characters and number + // + "account2": true, + + // Invalid: leading number + // + "1something": false, + + // Invalid: invalid character # + "_#1": false, + + // Invalid: various invalid characters + // + "!@#$%^&*": false, + } + + for name, validExpected := range names { + + code := fmt.Sprintf(`let %s = 1`, name) + + actual, err := testParseProgram(code) + + assert.NotNil(t, actual) + + if validExpected { + assert.NoError(t, err) + } else { + assert.IsType(t, Error{}, err) + } + } +} + +func TestParseArgumentList(t *testing.T) { + + t.Parallel() + + t.Run("invalid", func(t *testing.T) { + t.Parallel() + + _, errs := testParseArgumentList(`xyz`) + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "expected token '('", + Pos: ast.Position{Offset: 0, Line: 1, Column: 0}, + }, + }, + errs, + ) + }) + + t.Run("empty", func(t *testing.T) { + t.Parallel() + + result, errs := testParseArgumentList(`()`) + require.Empty(t, errs) + + var expected ast.Arguments + + utils.AssertEqualWithDiff(t, + expected, + result, + ) + }) + + t.Run("fatal error from lack of memory", func(t *testing.T) { + gauge := makeLimitingMemoryGauge() + gauge.Limit(common.MemoryKindTypeToken, 0) + + var panicMsg any + (func() { + defer func() { + panicMsg = recover() + }() + + ParseArgumentList(gauge, []byte(`(1, b: true)`), Config{}) + })() + + require.IsType(t, errors.MemoryError{}, panicMsg) + + fatalError, _ := panicMsg.(errors.MemoryError) + var expectedError limitingMemoryGaugeError + assert.ErrorAs(t, fatalError, &expectedError) + }) + + t.Run("valid", func(t *testing.T) { + t.Parallel() + + result, errs := testParseArgumentList(`(1, b: true)`) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + ast.Arguments{ + { + Label: "", + LabelStartPos: nil, + LabelEndPos: nil, + Expression: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{ + Offset: 1, + Line: 1, + Column: 1, + }, + EndPos: ast.Position{ + Offset: 1, + Line: 1, + Column: 1, + }, + }, + }, + TrailingSeparatorPos: ast.Position{ + Offset: 2, + Line: 1, + Column: 2, + }, + }, + { + Label: "b", + LabelStartPos: &ast.Position{ + Offset: 4, + Line: 1, + Column: 4, + }, + LabelEndPos: &ast.Position{ + Offset: 5, + Line: 1, + Column: 5, + }, + Expression: &ast.BoolExpression{ + Value: true, + Range: ast.Range{ + StartPos: ast.Position{ + Offset: 7, + Line: 1, + Column: 7, + }, + EndPos: ast.Position{ + Offset: 10, + Line: 1, + Column: 10, + }, + }, + }, + TrailingSeparatorPos: ast.Position{ + Offset: 11, + Line: 1, + Column: 11, + }, + }, + }, + result, + ) + }) + +} + +func TestParseBufferedErrors(t *testing.T) { + + t.Parallel() + + // Test that both top-level and buffered errors are reported. + // + // Test this using type argument lists, which are parsed through buffering: + // Only a subsequent open parenthesis will determine if a less-than sign + // introduced a type argument list of a function call, + // or if the expression is a less-than comparison. + // + // Inside the potential type argument list there is an error (missing type after comma), + // and outside (at the top-level, after buffering of the type argument list), + // there is another error (missing closing parenthesis after). + + _, errs := testParseExpression("a(") + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "missing type annotation after comma", + Pos: ast.Position{Offset: 4, Line: 1, Column: 4}, + }, + &SyntaxError{ + Message: "missing ')' at end of invocation argument list", + Pos: ast.Position{Offset: 6, Line: 1, Column: 6}, + }, + }, + errs, + ) +} + +func TestParseInvalidSingleQuoteImport(t *testing.T) { + + t.Parallel() + + _, err := testParseProgram(`import 'X'`) + + require.EqualError(t, err, "Parsing failed:\nerror: unrecognized character: U+0027 '''\n --> :1:7\n |\n1 | import 'X'\n | ^\n\nerror: unexpected end in import declaration: expected string, address, or identifier\n --> :1:7\n |\n1 | import 'X'\n | ^\n") +} + +func TestParseExpressionDepthLimit(t *testing.T) { + + t.Parallel() + + var builder strings.Builder + builder.WriteString("let x = y") + for i := 0; i < 20; i++ { + builder.WriteString(" ?? z") + } + + code := builder.String() + + _, err := testParseProgram(code) + require.Error(t, err) + + utils.AssertEqualWithDiff(t, + []error{ + ExpressionDepthLimitReachedError{ + Pos: ast.Position{ + Offset: 87, + Line: 1, + Column: 87, + }, + }, + }, + err.(Error).Errors, + ) +} + +func TestParseTypeDepthLimit(t *testing.T) { + + t.Parallel() + + const nesting = 20 + + var builder strings.Builder + builder.WriteString("let x: T<") + for i := 0; i < nesting; i++ { + builder.WriteString("T<") + } + builder.WriteString("U") + for i := 0; i < nesting; i++ { + builder.WriteString(">") + } + builder.WriteString(">? = nil") + + code := builder.String() + + _, err := testParseProgram(code) + require.Error(t, err) + + utils.AssertEqualWithDiff(t, + []error{ + TypeDepthLimitReachedError{ + Pos: ast.Position{ + Offset: 39, + Line: 1, + Column: 39, + }, + }, + }, + err.(Error).Errors, + ) +} + +func TestParseLocalReplayLimit(t *testing.T) { + t.Parallel() + + var builder strings.Builder + builder.WriteString("let t = T") + for i := 0; i < 30; i++ { + builder.WriteString("()") + + code := []byte(builder.String()) + _, err := ParseProgram(nil, code, Config{}) + utils.AssertEqualWithDiff(t, + Error{ + Code: code, + Errors: []error{ + &SyntaxError{ + Message: fmt.Sprintf( + "program too ambiguous, local replay limit of %d tokens exceeded", + localTokenReplayCountLimit, + ), + Pos: ast.Position{ + Offset: 44, + Line: 1, + Column: 44, + }, + }, + }, + }, + err, + ) +} + +func TestParseGlobalReplayLimit(t *testing.T) { + + t.Parallel() + + var builder strings.Builder + for j := 0; j < 2; j++ { + builder.WriteString(";let t = T") + for i := 0; i < 16; i++ { + builder.WriteString(" 1 { + previousStatement := statements[statementCount-2] + previousLine := previousStatement.EndPosition(p.memoryGauge).Line + currentStartPos := statement.StartPosition() + if previousLine == currentStartPos.Line { + p.report(NewSyntaxError( + currentStartPos, + "statements on the same line must be separated with a semicolon", + )) + } + } + } + + sawSemicolon = false + } + } +} + +func parseStatement(p *parser) (ast.Statement, error) { + p.skipSpaceAndComments() + + // It might start with a keyword for a statement + + switch p.current.Type { + case lexer.TokenIdentifier: + switch string(p.currentTokenSource()) { + case keywordReturn: + return parseReturnStatement(p) + case keywordBreak: + return parseBreakStatement(p), nil + case keywordContinue: + return parseContinueStatement(p), nil + case keywordIf: + return parseIfStatement(p) + case keywordSwitch: + return parseSwitchStatement(p) + case keywordWhile: + return parseWhileStatement(p) + case keywordFor: + return parseForStatement(p) + case keywordEmit: + return parseEmitStatement(p) + case keywordRemove: + return parseRemoveStatement(p) + case keywordFun: + // The `fun` keyword is ambiguous: it either introduces a function expression + // or a function declaration, depending on if an identifier follows, or not. + return parseFunctionDeclarationOrFunctionExpressionStatement(p) + } + } + + // If it is not a keyword for a statement, + // it might start with a keyword for a declaration + + declaration, err := parseDeclaration(p, "") + if err != nil { + return nil, err + } + + if statement, ok := declaration.(ast.Statement); ok { + return statement, nil + } + + // If it is not a statement or declaration, + // it must be an expression + + expression, err := parseExpression(p, lowestBindingPower) + if err != nil { + return nil, err + } + + // If the expression is followed by a transfer, + // it is actually the target of an assignment or swap statement + + p.skipSpaceAndComments() + switch p.current.Type { + case lexer.TokenEqual, lexer.TokenLeftArrow, lexer.TokenLeftArrowExclamation: + transfer := parseTransfer(p) + + value, err := parseExpression(p, lowestBindingPower) + if err != nil { + return nil, err + } + + return ast.NewAssignmentStatement(p.memoryGauge, expression, transfer, value), nil + + case lexer.TokenSwap: + p.next() + + right, err := parseExpression(p, lowestBindingPower) + if err != nil { + return nil, err + } + + return ast.NewSwapStatement(p.memoryGauge, expression, right), nil + + default: + return ast.NewExpressionStatement(p.memoryGauge, expression), nil + } +} + +func parseFunctionDeclarationOrFunctionExpressionStatement(p *parser) (ast.Statement, error) { + + startPos := p.current.StartPos + + // Skip the `fun` keyword + p.nextSemanticToken() + + if p.current.Is(lexer.TokenIdentifier) { + identifier := p.tokenToIdentifier(p.current) + + p.next() + + var typeParameterList *ast.TypeParameterList + + if p.config.TypeParametersEnabled { + var err error + typeParameterList, err = parseTypeParameterList(p) + if err != nil { + return nil, err + } + } + + parameterList, returnTypeAnnotation, functionBlock, err := + parseFunctionParameterListAndRest(p, false) + + if err != nil { + return nil, err + } + + return ast.NewFunctionDeclaration( + p.memoryGauge, + ast.AccessNotSpecified, + ast.FunctionPurityUnspecified, + false, + false, + identifier, + typeParameterList, + parameterList, + returnTypeAnnotation, + functionBlock, + startPos, + "", + ), nil + } else { + parameterList, returnTypeAnnotation, functionBlock, err := + parseFunctionParameterListAndRest(p, false) + if err != nil { + return nil, err + } + + return ast.NewExpressionStatement( + p.memoryGauge, + ast.NewFunctionExpression( + p.memoryGauge, + ast.FunctionPurityUnspecified, + parameterList, + returnTypeAnnotation, + functionBlock, + startPos, + ), + ), nil + } +} + +func parseReturnStatement(p *parser) (*ast.ReturnStatement, error) { + tokenRange := p.current.Range + endPosition := tokenRange.EndPos + p.next() + + sawNewLine, _ := p.parseTrivia(triviaOptions{ + skipNewlines: false, + }) + + var expression ast.Expression + var err error + switch p.current.Type { + case lexer.TokenEOF, lexer.TokenSemicolon, lexer.TokenBraceClose: + break + default: + if !sawNewLine { + expression, err = parseExpression(p, lowestBindingPower) + if err != nil { + return nil, err + } + + endPosition = expression.EndPosition(p.memoryGauge) + } + } + + return ast.NewReturnStatement( + p.memoryGauge, + expression, + ast.NewRange( + p.memoryGauge, + tokenRange.StartPos, + endPosition, + ), + ), nil +} + +func parseBreakStatement(p *parser) *ast.BreakStatement { + tokenRange := p.current.Range + p.next() + + return ast.NewBreakStatement(p.memoryGauge, tokenRange) +} + +func parseContinueStatement(p *parser) *ast.ContinueStatement { + tokenRange := p.current.Range + p.next() + + return ast.NewContinueStatement(p.memoryGauge, tokenRange) +} + +func parseIfStatement(p *parser) (*ast.IfStatement, error) { + + var ifStatements []*ast.IfStatement + + for { + startPos := p.current.StartPos + p.nextSemanticToken() + + var variableDeclaration *ast.VariableDeclaration + var err error + + if p.current.Type == lexer.TokenIdentifier { + switch string(p.currentTokenSource()) { + case keywordLet, keywordVar: + variableDeclaration, err = + parseVariableDeclaration(p, ast.AccessNotSpecified, nil, "") + if err != nil { + return nil, err + } + } + } + + var expression ast.Expression + + if variableDeclaration == nil { + expression, err = parseExpression(p, lowestBindingPower) + if err != nil { + return nil, err + } + } + + thenBlock, err := parseBlock(p) + if err != nil { + return nil, err + } + + var elseBlock *ast.Block + + parseNested := false + + p.skipSpaceAndComments() + if p.isToken(p.current, lexer.TokenIdentifier, keywordElse) { + p.nextSemanticToken() + if p.isToken(p.current, lexer.TokenIdentifier, keywordIf) { + parseNested = true + } else { + elseBlock, err = parseBlock(p) + if err != nil { + return nil, err + } + } + } + + var test ast.IfStatementTest + switch { + case variableDeclaration != nil: + test = variableDeclaration + case expression != nil: + test = expression + default: + panic(errors.NewUnreachableError()) + } + + ifStatement := ast.NewIfStatement( + p.memoryGauge, + test, + thenBlock, + elseBlock, + startPos, + ) + + if variableDeclaration != nil { + variableDeclaration.ParentIfStatement = ifStatement + } + + ifStatements = append(ifStatements, ifStatement) + + if !parseNested { + break + } + } + + length := len(ifStatements) + + result := ifStatements[length-1] + + for i := length - 2; i >= 0; i-- { + outer := ifStatements[i] + outer.Else = ast.NewBlock( + p.memoryGauge, + []ast.Statement{result}, + ast.NewRangeFromPositioned(p.memoryGauge, result), + ) + result = outer + } + + return result, nil +} + +func parseWhileStatement(p *parser) (*ast.WhileStatement, error) { + + startPos := p.current.StartPos + p.next() + + expression, err := parseExpression(p, lowestBindingPower) + if err != nil { + return nil, err + } + + block, err := parseBlock(p) + if err != nil { + return nil, err + } + + return ast.NewWhileStatement(p.memoryGauge, expression, block, startPos), nil +} + +func parseForStatement(p *parser) (*ast.ForStatement, error) { + + startPos := p.current.StartPos + p.nextSemanticToken() + + if p.isToken(p.current, lexer.TokenIdentifier, keywordIn) { + p.reportSyntaxError( + "expected identifier, got keyword %q", + keywordIn, + ) + p.next() + } + + firstValue, err := p.mustIdentifier() + if err != nil { + return nil, err + } + + p.skipSpaceAndComments() + + var index *ast.Identifier + var identifier ast.Identifier + + if p.current.Is(lexer.TokenComma) { + p.nextSemanticToken() + index = &firstValue + identifier, err = p.mustIdentifier() + if err != nil { + return nil, err + } + + p.skipSpaceAndComments() + } else { + identifier = firstValue + } + + if !p.isToken(p.current, lexer.TokenIdentifier, keywordIn) { + p.reportSyntaxError( + "expected keyword %q, got %s", + keywordIn, + p.current.Type, + ) + } + + p.next() + + expression, err := parseExpression(p, lowestBindingPower) + if err != nil { + return nil, err + } + + block, err := parseBlock(p) + if err != nil { + return nil, err + } + + return ast.NewForStatement( + p.memoryGauge, + identifier, + index, + block, + expression, + startPos, + ), nil +} + +func parseBlock(p *parser) (*ast.Block, error) { + startToken, err := p.mustOne(lexer.TokenBraceOpen) + if err != nil { + return nil, err + } + + statements, err := parseStatements(p, func(token lexer.Token) bool { + return token.Type == lexer.TokenBraceClose + }) + if err != nil { + return nil, err + } + + endToken, err := p.mustOne(lexer.TokenBraceClose) + if err != nil { + return nil, err + } + + return ast.NewBlock( + p.memoryGauge, + statements, + ast.NewRange( + p.memoryGauge, + startToken.StartPos, + endToken.EndPos, + ), + ), nil +} + +func parseFunctionBlock(p *parser) (*ast.FunctionBlock, error) { + p.skipSpaceAndComments() + + startToken, err := p.mustOne(lexer.TokenBraceOpen) + if err != nil { + return nil, err + } + + p.skipSpaceAndComments() + + var preConditions *ast.Conditions + if p.isToken(p.current, lexer.TokenIdentifier, keywordPre) { + p.next() + conditions, err := parseConditions(p, ast.ConditionKindPre) + if err != nil { + return nil, err + } + + preConditions = &conditions + } + + p.skipSpaceAndComments() + + var postConditions *ast.Conditions + if p.isToken(p.current, lexer.TokenIdentifier, keywordPost) { + p.next() + conditions, err := parseConditions(p, ast.ConditionKindPost) + if err != nil { + return nil, err + } + + postConditions = &conditions + } + + statements, err := parseStatements(p, func(token lexer.Token) bool { + return token.Type == lexer.TokenBraceClose + }) + if err != nil { + return nil, err + } + + endToken, err := p.mustOne(lexer.TokenBraceClose) + if err != nil { + return nil, err + } + + return ast.NewFunctionBlock( + p.memoryGauge, + ast.NewBlock( + p.memoryGauge, + statements, + ast.NewRange( + p.memoryGauge, + startToken.StartPos, + endToken.EndPos, + ), + ), + preConditions, + postConditions, + ), nil +} + +// parseConditions parses conditions (pre/post) +func parseConditions(p *parser, kind ast.ConditionKind) (conditions ast.Conditions, err error) { + + p.skipSpaceAndComments() + _, err = p.mustOne(lexer.TokenBraceOpen) + if err != nil { + return nil, err + } + + defer func() { + p.skipSpaceAndComments() + _, err = p.mustOne(lexer.TokenBraceClose) + }() + + for { + p.skipSpaceAndComments() + switch p.current.Type { + case lexer.TokenSemicolon: + p.next() + continue + + case lexer.TokenBraceClose, lexer.TokenEOF: + return + + default: + var condition ast.Condition + condition, err = parseCondition(p, kind) + if err != nil || condition == nil { + return + } + + conditions = append(conditions, condition) + } + } +} + +// parseCondition parses a condition (pre/post) +// +// condition : expression (':' expression )? +func parseCondition(p *parser, kind ast.ConditionKind) (ast.Condition, error) { + + test, err := parseExpression(p, lowestBindingPower) + if err != nil { + return nil, err + } + + p.skipSpaceAndComments() + + var message ast.Expression + if p.current.Is(lexer.TokenColon) { + p.next() + + message, err = parseExpression(p, lowestBindingPower) + if err != nil { + return nil, err + } + } + + return &ast.TestCondition{ + Test: test, + Message: message, + }, nil +} + +func parseEmitStatement(p *parser) (*ast.EmitStatement, error) { + startPos := p.current.StartPos + p.next() + + invocation, err := parseNominalTypeInvocationRemainder(p) + if err != nil { + return nil, err + } + + return ast.NewEmitStatement(p.memoryGauge, invocation, startPos), nil +} + +func parseSwitchStatement(p *parser) (*ast.SwitchStatement, error) { + + startPos := p.current.StartPos + + // Skip the `switch` keyword + p.next() + + expression, err := parseExpression(p, lowestBindingPower) + if err != nil { + return nil, err + } + + _, err = p.mustOne(lexer.TokenBraceOpen) + if err != nil { + return nil, err + } + + cases, err := parseSwitchCases(p) + if err != nil { + return nil, err + } + + endToken, err := p.mustOne(lexer.TokenBraceClose) + if err != nil { + return nil, err + } + + return ast.NewSwitchStatement( + p.memoryGauge, + expression, + cases, + ast.NewRange( + p.memoryGauge, + startPos, + endToken.EndPos, + ), + ), nil +} + +// parseSwitchCases parses cases of a switch statement. +// +// switchCases : switchCase* +func parseSwitchCases(p *parser) (cases []*ast.SwitchCase, err error) { + + reportUnexpected := func() { + p.reportSyntaxError( + "unexpected token: got %s, expected %q or %q", + p.current.Type, + keywordCase, + keywordDefault, + ) + p.next() + } + + for { + p.skipSpaceAndComments() + + switch p.current.Type { + case lexer.TokenIdentifier: + + var switchCase *ast.SwitchCase + switch string(p.currentTokenSource()) { + case keywordCase: + switchCase, err = parseSwitchCase(p, true) + + case keywordDefault: + switchCase, err = parseSwitchCase(p, false) + + default: + reportUnexpected() + continue + } + + if err != nil { + return + } + + cases = append(cases, switchCase) + + case lexer.TokenBraceClose, lexer.TokenEOF: + return + + default: + reportUnexpected() + } + } +} + +// parseSwitchCase parses a switch case (hasExpression == true) +// or default case (hasExpression == false) +// +// switchCase : `case` expression `:` statements +// | `default` `:` statements +func parseSwitchCase(p *parser, hasExpression bool) (*ast.SwitchCase, error) { + + startPos := p.current.StartPos + + // Skip the keyword + p.next() + + var expression ast.Expression + var err error + + if hasExpression { + expression, err = parseExpression(p, lowestBindingPower) + if err != nil { + return nil, err + } + } else { + p.skipSpaceAndComments() + } + + colonPos := p.current.StartPos + + if !p.current.Is(lexer.TokenColon) { + p.reportSyntaxError( + "expected %s, got %s", + lexer.TokenColon, + p.current.Type, + ) + } + + p.next() + + statements, err := parseStatements(p, func(token lexer.Token) bool { + switch token.Type { + case lexer.TokenBraceClose: + return true + + case lexer.TokenIdentifier: + switch string(p.currentTokenSource()) { + case keywordCase, keywordDefault: + return true + default: + return false + } + + default: + return false + } + }) + if err != nil { + return nil, err + } + + endPos := colonPos + + if len(statements) > 0 { + lastStatementIndex := len(statements) - 1 + endPos = statements[lastStatementIndex].EndPosition(p.memoryGauge) + } + + return &ast.SwitchCase{ + Expression: expression, + Statements: statements, + Range: ast.NewRange( + p.memoryGauge, + startPos, + endPos, + ), + }, nil +} + +func parseRemoveStatement( + p *parser, +) (*ast.RemoveStatement, error) { + + startPos := p.current.StartPos + p.next() + p.skipSpaceAndComments() + + attachment, err := parseType(p, lowestBindingPower) + if err != nil { + return nil, err + } + attachmentNominalType, ok := attachment.(*ast.NominalType) + + if !ok { + p.reportSyntaxError( + "expected attachment nominal type, got %s", + attachment, + ) + } + + p.skipSpaceAndComments() + + // check and skip `from` keyword + if !p.isToken(p.current, lexer.TokenIdentifier, keywordFrom) { + p.reportSyntaxError( + "expected from keyword, got %s", + p.current.Type, + ) + } + p.nextSemanticToken() + + attached, err := parseExpression(p, lowestBindingPower) + if err != nil { + return nil, err + } + + return ast.NewRemoveStatement( + p.memoryGauge, + attachmentNominalType, + attached, + startPos, + ), nil +} diff --git a/runtime/old_parser/statement_test.go b/runtime/old_parser/statement_test.go new file mode 100644 index 0000000000..1c86eb3765 --- /dev/null +++ b/runtime/old_parser/statement_test.go @@ -0,0 +1,2500 @@ +/* + * Cadence - The resource-oriented smart contract programming language + * + * Copyright Dapper Labs, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package old_parser + +import ( + "math/big" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/onflow/cadence/runtime/ast" + "github.com/onflow/cadence/runtime/tests/utils" +) + +func TestParseReplInput(t *testing.T) { + + t.Parallel() + + actual, errs := testParseStatements(` + struct X {}; let x = X(); x + `) + + var err error + if len(errs) > 0 { + err = Error{ + Errors: errs, + } + } + + require.NoError(t, err) + require.IsType(t, []ast.Statement{}, actual) + + require.Len(t, actual, 3) + assert.IsType(t, &ast.CompositeDeclaration{}, actual[0]) + assert.IsType(t, &ast.VariableDeclaration{}, actual[1]) + assert.IsType(t, &ast.ExpressionStatement{}, actual[2]) +} +func TestParseReturnStatement(t *testing.T) { + + t.Parallel() + + t.Run("no expression", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseStatements("return") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Statement{ + &ast.ReturnStatement{ + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 5, Offset: 5}, + }, + }, + }, + result, + ) + }) + + t.Run("expression on same line", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseStatements("return 1") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Statement{ + &ast.ReturnStatement{ + Expression: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 7, Offset: 7}, + EndPos: ast.Position{Line: 1, Column: 7, Offset: 7}, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 7, Offset: 7}, + }, + }, + }, + result, + ) + }) + + t.Run("expression on next line, no semicolon", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseStatements("return \n1") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Statement{ + &ast.ReturnStatement{ + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 5, Offset: 5}, + }, + }, + &ast.ExpressionStatement{ + Expression: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 2, Column: 0, Offset: 8}, + EndPos: ast.Position{Line: 2, Column: 0, Offset: 8}, + }, + }, + }, + }, + result, + ) + }) + + t.Run("expression on next line, semicolon", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseStatements("return ;\n1") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Statement{ + &ast.ReturnStatement{ + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 5, Offset: 5}, + }, + }, + &ast.ExpressionStatement{ + Expression: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 2, Column: 0, Offset: 9}, + EndPos: ast.Position{Line: 2, Column: 0, Offset: 9}, + }, + }, + }, + }, + result, + ) + }) +} + +func TestParseIfStatement(t *testing.T) { + + t.Parallel() + + t.Run("only empty then", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseStatements("if true { }") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Statement{ + &ast.IfStatement{ + Test: &ast.BoolExpression{ + Value: true, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 3, Offset: 3}, + EndPos: ast.Position{Line: 1, Column: 6, Offset: 6}, + }, + }, + Then: &ast.Block{ + Statements: nil, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 8, Offset: 8}, + EndPos: ast.Position{Line: 1, Column: 10, Offset: 10}, + }, + }, + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + result, + ) + }) + + t.Run("only then, two statements on one line", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseStatements("if true { 1 ; 2 }") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Statement{ + &ast.IfStatement{ + Test: &ast.BoolExpression{ + Value: true, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 3, Offset: 3}, + EndPos: ast.Position{Line: 1, Column: 6, Offset: 6}, + }, + }, + Then: &ast.Block{ + Statements: []ast.Statement{ + &ast.ExpressionStatement{ + Expression: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 10, Offset: 10}, + EndPos: ast.Position{Line: 1, Column: 10, Offset: 10}, + }, + }, + }, + &ast.ExpressionStatement{ + Expression: &ast.IntegerExpression{ + PositiveLiteral: []byte("2"), + Value: big.NewInt(2), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 14, Offset: 14}, + EndPos: ast.Position{Line: 1, Column: 14, Offset: 14}, + }, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 8, Offset: 8}, + EndPos: ast.Position{Line: 1, Column: 16, Offset: 16}, + }, + }, + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + result, + ) + }) + + t.Run("only then, two statements on multiple lines", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseStatements("if true { 1 \n 2 }") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Statement{ + &ast.IfStatement{ + Test: &ast.BoolExpression{ + Value: true, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 3, Offset: 3}, + EndPos: ast.Position{Line: 1, Column: 6, Offset: 6}, + }, + }, + Then: &ast.Block{ + Statements: []ast.Statement{ + &ast.ExpressionStatement{ + Expression: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 10, Offset: 10}, + EndPos: ast.Position{Line: 1, Column: 10, Offset: 10}, + }, + }, + }, + &ast.ExpressionStatement{ + Expression: &ast.IntegerExpression{ + PositiveLiteral: []byte("2"), + Value: big.NewInt(2), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 2, Column: 1, Offset: 14}, + EndPos: ast.Position{Line: 2, Column: 1, Offset: 14}, + }, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 8, Offset: 8}, + EndPos: ast.Position{Line: 2, Column: 3, Offset: 16}, + }, + }, + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + result, + ) + }) + + t.Run("with else", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseStatements("if true { 1 } else { 2 }") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Statement{ + &ast.IfStatement{ + Test: &ast.BoolExpression{ + Value: true, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 3, Offset: 3}, + EndPos: ast.Position{Line: 1, Column: 6, Offset: 6}, + }, + }, + Then: &ast.Block{ + Statements: []ast.Statement{ + &ast.ExpressionStatement{ + Expression: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 10, Offset: 10}, + EndPos: ast.Position{Line: 1, Column: 10, Offset: 10}, + }, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 8, Offset: 8}, + EndPos: ast.Position{Line: 1, Column: 12, Offset: 12}, + }, + }, + Else: &ast.Block{ + Statements: []ast.Statement{ + &ast.ExpressionStatement{ + Expression: &ast.IntegerExpression{ + PositiveLiteral: []byte("2"), + Value: big.NewInt(2), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 21, Offset: 21}, + EndPos: ast.Position{Line: 1, Column: 21, Offset: 21}, + }, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 19, Offset: 19}, + EndPos: ast.Position{Line: 1, Column: 23, Offset: 23}, + }, + }, + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + result, + ) + }) + + t.Run("with else if and else, no space", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseStatements("if true{1}else if true {2} else{3}") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Statement{ + &ast.IfStatement{ + Test: &ast.BoolExpression{ + Value: true, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 3, Offset: 3}, + EndPos: ast.Position{Line: 1, Column: 6, Offset: 6}, + }, + }, + Then: &ast.Block{ + Statements: []ast.Statement{ + &ast.ExpressionStatement{ + Expression: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 8, Offset: 8}, + EndPos: ast.Position{Line: 1, Column: 8, Offset: 8}, + }, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 7, Offset: 7}, + EndPos: ast.Position{Line: 1, Column: 9, Offset: 9}, + }, + }, + Else: &ast.Block{ + Statements: []ast.Statement{ + &ast.IfStatement{ + Test: &ast.BoolExpression{ + Value: true, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 18, Offset: 18}, + EndPos: ast.Position{Line: 1, Column: 21, Offset: 21}, + }, + }, + Then: &ast.Block{ + Statements: []ast.Statement{ + &ast.ExpressionStatement{ + Expression: &ast.IntegerExpression{ + PositiveLiteral: []byte("2"), + Value: big.NewInt(2), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 24, Offset: 24}, + EndPos: ast.Position{Line: 1, Column: 24, Offset: 24}, + }, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 23, Offset: 23}, + EndPos: ast.Position{Line: 1, Column: 25, Offset: 25}, + }, + }, + Else: &ast.Block{ + Statements: []ast.Statement{ + &ast.ExpressionStatement{ + Expression: &ast.IntegerExpression{ + PositiveLiteral: []byte("3"), + Value: big.NewInt(3), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 32, Offset: 32}, + EndPos: ast.Position{Line: 1, Column: 32, Offset: 32}, + }, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 31, Offset: 31}, + EndPos: ast.Position{Line: 1, Column: 33, Offset: 33}, + }, + }, + StartPos: ast.Position{Line: 1, Column: 15, Offset: 15}, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 15, Offset: 15}, + EndPos: ast.Position{Line: 1, Column: 33, Offset: 33}, + }, + }, + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + result, + ) + }) + + t.Run("if-var", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseStatements("if var x = 1 { }") + require.Empty(t, errs) + + expected := &ast.IfStatement{ + Test: &ast.VariableDeclaration{ + Access: ast.AccessNotSpecified, + IsConstant: false, + Identifier: ast.Identifier{ + Identifier: "x", + Pos: ast.Position{Line: 1, Column: 7, Offset: 7}, + }, + Value: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 11, Offset: 11}, + EndPos: ast.Position{Line: 1, Column: 11, Offset: 11}, + }, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationCopy, + Pos: ast.Position{Line: 1, Column: 9, Offset: 9}, + }, + StartPos: ast.Position{Line: 1, Column: 3, Offset: 3}, + }, + Then: &ast.Block{ + Statements: nil, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 13, Offset: 13}, + EndPos: ast.Position{Line: 1, Column: 15, Offset: 15}, + }, + }, + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + } + + expected.Test.(*ast.VariableDeclaration).ParentIfStatement = expected + + utils.AssertEqualWithDiff(t, + []ast.Statement{ + expected, + }, + result, + ) + }) + + t.Run("if-let", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseStatements("if let x = 1 { }") + require.Empty(t, errs) + + expected := &ast.IfStatement{ + Test: &ast.VariableDeclaration{ + Access: ast.AccessNotSpecified, + IsConstant: true, + Identifier: ast.Identifier{ + Identifier: "x", + Pos: ast.Position{Line: 1, Column: 7, Offset: 7}, + }, + Value: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 11, Offset: 11}, + EndPos: ast.Position{Line: 1, Column: 11, Offset: 11}, + }, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationCopy, + Pos: ast.Position{Line: 1, Column: 9, Offset: 9}, + }, + StartPos: ast.Position{Line: 1, Column: 3, Offset: 3}, + }, + Then: &ast.Block{ + Statements: nil, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 13, Offset: 13}, + EndPos: ast.Position{Line: 1, Column: 15, Offset: 15}, + }, + }, + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + } + + expected.Test.(*ast.VariableDeclaration).ParentIfStatement = expected + + utils.AssertEqualWithDiff(t, + []ast.Statement{ + expected, + }, + result, + ) + }) + +} + +func TestParseWhileStatement(t *testing.T) { + + t.Parallel() + + t.Run("empty block", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseStatements("while true { }") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Statement{ + &ast.WhileStatement{ + Test: &ast.BoolExpression{ + Value: true, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 6, Offset: 6}, + EndPos: ast.Position{Line: 1, Column: 9, Offset: 9}, + }, + }, + Block: &ast.Block{ + Statements: nil, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 11, Offset: 11}, + EndPos: ast.Position{Line: 1, Column: 13, Offset: 13}, + }, + }, + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + result, + ) + }) +} + +func TestParseAssignmentStatement(t *testing.T) { + + t.Parallel() + + t.Run("copy, no space", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseStatements("x=1") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Statement{ + &ast.AssignmentStatement{ + Target: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "x", + Pos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationCopy, + Pos: ast.Position{Line: 1, Column: 1, Offset: 1}, + }, + Value: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + EndPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + }, + }, + }, + }, + result, + ) + }) + + t.Run("copy, spaces", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseStatements(" x = 1") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Statement{ + &ast.AssignmentStatement{ + Target: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "x", + Pos: ast.Position{Line: 1, Column: 1, Offset: 1}, + }, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationCopy, + Pos: ast.Position{Line: 1, Column: 3, Offset: 3}, + }, + Value: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 5, Offset: 5}, + EndPos: ast.Position{Line: 1, Column: 5, Offset: 5}, + }, + }, + }, + }, + result, + ) + }) + + t.Run("move", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseStatements(" x <- 1") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Statement{ + &ast.AssignmentStatement{ + Target: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "x", + Pos: ast.Position{Line: 1, Column: 1, Offset: 1}, + }, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationMove, + Pos: ast.Position{Line: 1, Column: 3, Offset: 3}, + }, + Value: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 6, Offset: 6}, + EndPos: ast.Position{Line: 1, Column: 6, Offset: 6}, + }, + }, + }, + }, + result, + ) + }) + + t.Run("force move", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseStatements(" x <-! 1") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Statement{ + &ast.AssignmentStatement{ + Target: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "x", + Pos: ast.Position{Line: 1, Column: 1, Offset: 1}, + }, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationMoveForced, + Pos: ast.Position{Line: 1, Column: 3, Offset: 3}, + }, + Value: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 7, Offset: 7}, + EndPos: ast.Position{Line: 1, Column: 7, Offset: 7}, + }, + }, + }, + }, + result, + ) + }) +} + +func TestParseSwapStatement(t *testing.T) { + + t.Parallel() + + t.Run("simple", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseStatements(" x <-> y") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Statement{ + &ast.SwapStatement{ + Left: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "x", + Pos: ast.Position{Line: 1, Column: 1, Offset: 1}, + }, + }, + Right: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "y", + Pos: ast.Position{Line: 1, Column: 7, Offset: 7}, + }, + }, + }, + }, + result, + ) + }) +} + +func TestParseForStatement(t *testing.T) { + + t.Parallel() + + t.Run("empty block", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseStatements("for x in y { }") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Statement{ + &ast.ForStatement{ + Identifier: ast.Identifier{ + Identifier: "x", + Pos: ast.Position{Line: 1, Column: 4, Offset: 4}, + }, + Value: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "y", + Pos: ast.Position{Line: 1, Column: 9, Offset: 9}, + }, + }, + Block: &ast.Block{ + Statements: nil, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 11, Offset: 11}, + EndPos: ast.Position{Line: 1, Column: 13, Offset: 13}, + }, + }, + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + result, + ) + }) +} + +func TestParseForStatementIndexBinding(t *testing.T) { + + t.Parallel() + + t.Run("empty block", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseStatements("for i, x in y { }") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Statement{ + &ast.ForStatement{ + Identifier: ast.Identifier{ + Identifier: "x", + Pos: ast.Position{Line: 1, Column: 7, Offset: 7}, + }, + Index: &ast.Identifier{ + Identifier: "i", + Pos: ast.Position{Line: 1, Column: 4, Offset: 4}, + }, + Value: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "y", + Pos: ast.Position{Line: 1, Column: 12, Offset: 12}, + }, + }, + Block: &ast.Block{ + Statements: nil, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 14, Offset: 14}, + EndPos: ast.Position{Line: 1, Column: 16, Offset: 16}, + }, + }, + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + result, + ) + }) + + t.Run("no comma", func(t *testing.T) { + + t.Parallel() + + _, errs := testParseStatements("for i x in y { }") + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "expected keyword \"in\", got identifier", + Pos: ast.Position{Offset: 6, Line: 1, Column: 6}, + }, + &SyntaxError{ + Message: "expected token '{'", + Pos: ast.Position{Offset: 11, Line: 1, Column: 11}, + }, + }, + errs, + ) + }) + + t.Run("no identifiers", func(t *testing.T) { + + t.Parallel() + + _, errs := testParseStatements("for in y { }") + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "expected identifier, got keyword \"in\"", + Pos: ast.Position{Offset: 4, Line: 1, Column: 4}, + }, + &SyntaxError{ + Message: "expected token identifier", + Pos: ast.Position{Offset: 6, Line: 1, Column: 6}, + }, + }, + errs, + ) + }) +} + +func TestParseEmit(t *testing.T) { + + t.Parallel() + + t.Run("simple", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseStatements("emit T()") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Statement{ + &ast.EmitStatement{ + InvocationExpression: &ast.InvocationExpression{ + InvokedExpression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "T", + Pos: ast.Position{Line: 1, Column: 5, Offset: 5}, + }, + }, + ArgumentsStartPos: ast.Position{Line: 1, Column: 6, Offset: 6}, + EndPos: ast.Position{Line: 1, Column: 7, Offset: 7}, + }, + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + result, + ) + }) +} + +func TestParseFunctionStatementOrExpression(t *testing.T) { + + t.Parallel() + + t.Run("function declaration with name", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseStatements("fun foo() {}") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Statement{ + &ast.FunctionDeclaration{ + Access: ast.AccessNotSpecified, + Identifier: ast.Identifier{ + Identifier: "foo", + Pos: ast.Position{Line: 1, Column: 4, Offset: 4}, + }, + ParameterList: &ast.ParameterList{ + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 7, Offset: 7}, + EndPos: ast.Position{Line: 1, Column: 8, Offset: 8}, + }, + }, + FunctionBlock: &ast.FunctionBlock{ + Block: &ast.Block{ + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 10, Offset: 10}, + EndPos: ast.Position{Line: 1, Column: 11, Offset: 11}, + }, + }, + }, + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + result, + ) + }) + + t.Run("function expression without name", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseStatements("fun () {}") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Statement{ + &ast.ExpressionStatement{ + Expression: &ast.FunctionExpression{ + ParameterList: &ast.ParameterList{ + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + EndPos: ast.Position{Line: 1, Column: 5, Offset: 5}, + }, + }, + FunctionBlock: &ast.FunctionBlock{ + Block: &ast.Block{ + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 7, Offset: 7}, + EndPos: ast.Position{Line: 1, Column: 8, Offset: 8}, + }, + }, + }, + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + }, + result, + ) + }) +} + +func TestParseStatements(t *testing.T) { + + t.Parallel() + + t.Run("binary expression with less operator", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseStatements("a + b < c\nd") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Statement{ + &ast.ExpressionStatement{ + Expression: &ast.BinaryExpression{ + Operation: ast.OperationLess, + Left: &ast.BinaryExpression{ + Operation: ast.OperationPlus, + Left: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "a", + Pos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + Right: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "b", + Pos: ast.Position{Line: 1, Column: 4, Offset: 4}, + }, + }, + }, + Right: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "c", + Pos: ast.Position{Line: 1, Column: 8, Offset: 8}, + }, + }, + }, + }, + &ast.ExpressionStatement{ + Expression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "d", + Pos: ast.Position{Line: 2, Column: 0, Offset: 10}, + }, + }, + }, + }, + result, + ) + }) + + t.Run("multiple statements on same line without semicolon", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseStatements(`assert true`) + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "statements on the same line must be separated with a semicolon", + Pos: ast.Position{Offset: 7, Line: 1, Column: 7}, + }, + }, + errs, + ) + + utils.AssertEqualWithDiff(t, + []ast.Statement{ + &ast.ExpressionStatement{ + Expression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "assert", + Pos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + }, + &ast.ExpressionStatement{ + Expression: &ast.BoolExpression{ + Value: true, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 7, Offset: 7}, + EndPos: ast.Position{Line: 1, Column: 10, Offset: 10}, + }, + }, + }, + }, + result, + ) + }) +} + +func TestParseRemoveAttachmentStatement(t *testing.T) { + + t.Parallel() + + t.Run("basic", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseStatements("remove A from b") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Statement{ + &ast.RemoveStatement{ + Attachment: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "A", + Pos: ast.Position{Line: 1, Column: 7, Offset: 7}, + }, + }, + Value: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "b", + Pos: ast.Position{Line: 1, Column: 14, Offset: 14}, + }, + }, + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + result, + ) + }) + + t.Run("namespaced attachment", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseStatements("remove Foo.E from b") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Statement{ + &ast.RemoveStatement{ + Attachment: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Foo", + Pos: ast.Position{Line: 1, Column: 7, Offset: 7}, + }, + NestedIdentifiers: []ast.Identifier{ + { + Identifier: "E", + Pos: ast.Position{Line: 1, Column: 11, Offset: 11}, + }, + }, + }, + Value: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "b", + Pos: ast.Position{Line: 1, Column: 18, Offset: 18}, + }, + }, + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + result, + ) + }) + + t.Run("no from", func(t *testing.T) { + + t.Parallel() + + _, errs := testParseStatements("remove A") + + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "expected from keyword, got EOF", + Pos: ast.Position{Offset: 8, Line: 1, Column: 8}, + }, + &SyntaxError{ + Message: "unexpected end of program", + Pos: ast.Position{Offset: 8, Line: 1, Column: 8}, + }, + }, + errs, + ) + }) + + t.Run("no target", func(t *testing.T) { + + t.Parallel() + + _, errs := testParseStatements("remove A from") + + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "unexpected end of program", + Pos: ast.Position{Offset: 13, Line: 1, Column: 13}, + }, + }, + errs, + ) + }) + + t.Run("no nominal type", func(t *testing.T) { + + t.Parallel() + + _, errs := testParseStatements("remove [A] from e") + + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "expected attachment nominal type, got [A]", + Pos: ast.Position{Offset: 10, Line: 1, Column: 10}, + }, + }, + errs, + ) + }) + + t.Run("complex source", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseStatements("remove A from foo()") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Statement{ + &ast.RemoveStatement{ + Attachment: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "A", + Pos: ast.Position{Line: 1, Column: 7, Offset: 7}, + }, + }, + Value: &ast.InvocationExpression{ + InvokedExpression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "foo", + Pos: ast.Position{Line: 1, Column: 14, Offset: 14}, + }, + }, + ArgumentsStartPos: ast.Position{Line: 1, Column: 17, Offset: 17}, + EndPos: ast.Position{Line: 1, Column: 18, Offset: 18}, + }, + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + result, + ) + }) +} + +func TestParseSwitchStatement(t *testing.T) { + + t.Parallel() + + t.Run("empty", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseStatements("switch true { }") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Statement{ + &ast.SwitchStatement{ + Expression: &ast.BoolExpression{ + Value: true, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 7, Offset: 7}, + EndPos: ast.Position{Line: 1, Column: 10, Offset: 10}, + }, + }, + Cases: nil, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 14, Offset: 14}, + }, + }, + }, + result, + ) + }) + + t.Run("two cases", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseStatements("switch x { case 1 :\n a\nb default : c\nd }") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Statement{ + &ast.SwitchStatement{ + Expression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "x", + Pos: ast.Position{Line: 1, Column: 7, Offset: 7}, + }, + }, + Cases: []*ast.SwitchCase{ + { + Expression: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 16, Offset: 16}, + EndPos: ast.Position{Line: 1, Column: 16, Offset: 16}, + }, + }, + Statements: []ast.Statement{ + &ast.ExpressionStatement{ + Expression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "a", + Pos: ast.Position{Line: 2, Column: 1, Offset: 21}, + }, + }, + }, + &ast.ExpressionStatement{ + Expression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "b", + Pos: ast.Position{Line: 3, Column: 0, Offset: 23}, + }, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 11, Offset: 11}, + EndPos: ast.Position{Line: 3, Column: 0, Offset: 23}, + }, + }, + { + Statements: []ast.Statement{ + &ast.ExpressionStatement{ + Expression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "c", + Pos: ast.Position{Line: 3, Column: 12, Offset: 35}, + }, + }, + }, + &ast.ExpressionStatement{ + Expression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "d", + Pos: ast.Position{Line: 4, Column: 0, Offset: 37}, + }, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Line: 3, Column: 2, Offset: 25}, + EndPos: ast.Position{Line: 4, Column: 0, Offset: 37}, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 4, Column: 3, Offset: 40}, + }, + }, + }, + result, + ) + }) +} + +func TestParseIfStatementInFunctionDeclaration(t *testing.T) { + + t.Parallel() + + const code = ` + fun test() { + if true { + return + } else if false { + false + 1 + } else { + 2 + } + } + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.FunctionDeclaration{ + Access: ast.AccessNotSpecified, + Identifier: ast.Identifier{ + Identifier: "test", + Pos: ast.Position{Offset: 10, Line: 2, Column: 9}, + }, + ParameterList: &ast.ParameterList{ + Range: ast.Range{ + StartPos: ast.Position{Offset: 14, Line: 2, Column: 13}, + EndPos: ast.Position{Offset: 15, Line: 2, Column: 14}, + }, + }, + FunctionBlock: &ast.FunctionBlock{ + Block: &ast.Block{ + Statements: []ast.Statement{ + &ast.IfStatement{ + Test: &ast.BoolExpression{ + Value: true, + Range: ast.Range{ + StartPos: ast.Position{Offset: 34, Line: 3, Column: 15}, + EndPos: ast.Position{Offset: 37, Line: 3, Column: 18}, + }, + }, + Then: &ast.Block{ + Statements: []ast.Statement{ + &ast.ReturnStatement{ + Expression: nil, + Range: ast.Range{ + StartPos: ast.Position{Offset: 57, Line: 4, Column: 16}, + EndPos: ast.Position{Offset: 62, Line: 4, Column: 21}, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 39, Line: 3, Column: 20}, + EndPos: ast.Position{Offset: 76, Line: 5, Column: 12}, + }, + }, + Else: &ast.Block{ + Statements: []ast.Statement{ + &ast.IfStatement{ + Test: &ast.BoolExpression{ + Value: false, + Range: ast.Range{ + StartPos: ast.Position{Offset: 86, Line: 5, Column: 22}, + EndPos: ast.Position{Offset: 90, Line: 5, Column: 26}, + }, + }, + Then: &ast.Block{ + Statements: []ast.Statement{ + &ast.ExpressionStatement{ + Expression: &ast.BoolExpression{ + Value: false, + Range: ast.Range{ + StartPos: ast.Position{Offset: 110, Line: 6, Column: 16}, + EndPos: ast.Position{Offset: 114, Line: 6, Column: 20}, + }, + }, + }, + &ast.ExpressionStatement{ + Expression: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 132, Line: 7, Column: 16}, + EndPos: ast.Position{Offset: 132, Line: 7, Column: 16}, + }, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 92, Line: 5, Column: 28}, + EndPos: ast.Position{Offset: 146, Line: 8, Column: 12}, + }, + }, + Else: &ast.Block{ + Statements: []ast.Statement{ + &ast.ExpressionStatement{ + Expression: &ast.IntegerExpression{ + PositiveLiteral: []byte("2"), + Value: big.NewInt(2), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 171, Line: 9, Column: 16}, + EndPos: ast.Position{Offset: 171, Line: 9, Column: 16}, + }, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 153, Line: 8, Column: 19}, + EndPos: ast.Position{Offset: 185, Line: 10, Column: 12}, + }, + }, + StartPos: ast.Position{Offset: 83, Line: 5, Column: 19}, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 83, Line: 5, Column: 19}, + EndPos: ast.Position{Offset: 185, Line: 10, Column: 12}, + }, + }, + StartPos: ast.Position{Offset: 31, Line: 3, Column: 12}, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 17, Line: 2, Column: 16}, + EndPos: ast.Position{Offset: 195, Line: 11, Column: 8}, + }, + }, + }, + StartPos: ast.Position{Offset: 6, Line: 2, Column: 5}, + }, + }, + result.Declarations(), + ) +} + +func TestParseIfStatementWithVariableDeclaration(t *testing.T) { + + t.Parallel() + + const code = ` + fun test() { + if var y = x { + 1 + } else { + 2 + } + } + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + ifStatement := &ast.IfStatement{ + Then: &ast.Block{ + Statements: []ast.Statement{ + &ast.ExpressionStatement{ + Expression: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 62, Line: 4, Column: 16}, + EndPos: ast.Position{Offset: 62, Line: 4, Column: 16}, + }, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 44, Line: 3, Column: 25}, + EndPos: ast.Position{Offset: 76, Line: 5, Column: 12}, + }, + }, + Else: &ast.Block{ + Statements: []ast.Statement{ + &ast.ExpressionStatement{ + Expression: &ast.IntegerExpression{ + PositiveLiteral: []byte("2"), + Value: big.NewInt(2), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 101, Line: 6, Column: 16}, + EndPos: ast.Position{Offset: 101, Line: 6, Column: 16}, + }, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 83, Line: 5, Column: 19}, + EndPos: ast.Position{Offset: 115, Line: 7, Column: 12}, + }, + }, + StartPos: ast.Position{Offset: 31, Line: 3, Column: 12}, + } + + ifTestVariableDeclaration := &ast.VariableDeclaration{ + Access: ast.AccessNotSpecified, + IsConstant: false, + Identifier: ast.Identifier{ + Identifier: "y", + Pos: ast.Position{Offset: 38, Line: 3, Column: 19}, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationCopy, + Pos: ast.Position{Offset: 40, Line: 3, Column: 21}, + }, + Value: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "x", + Pos: ast.Position{Offset: 42, Line: 3, Column: 23}, + }, + }, + StartPos: ast.Position{Offset: 34, Line: 3, Column: 15}, + ParentIfStatement: ifStatement, + } + + ifStatement.Test = ifTestVariableDeclaration + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.FunctionDeclaration{ + Access: ast.AccessNotSpecified, + Identifier: ast.Identifier{ + Identifier: "test", + Pos: ast.Position{Offset: 10, Line: 2, Column: 9}, + }, + ParameterList: &ast.ParameterList{ + Range: ast.Range{ + StartPos: ast.Position{Offset: 14, Line: 2, Column: 13}, + EndPos: ast.Position{Offset: 15, Line: 2, Column: 14}, + }, + }, + FunctionBlock: &ast.FunctionBlock{ + Block: &ast.Block{ + Statements: []ast.Statement{ + ifStatement, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 17, Line: 2, Column: 16}, + EndPos: ast.Position{Offset: 125, Line: 8, Column: 8}, + }, + }, + }, + StartPos: ast.Position{Offset: 6, Line: 2, Column: 5}, + }, + }, + result.Declarations(), + ) +} + +func TestParseIfStatementNoElse(t *testing.T) { + + t.Parallel() + + const code = ` + fun test() { + if true { + return + } + } + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.FunctionDeclaration{ + Access: ast.AccessNotSpecified, + Identifier: ast.Identifier{ + Identifier: "test", + Pos: ast.Position{Offset: 10, Line: 2, Column: 9}, + }, + ParameterList: &ast.ParameterList{ + Range: ast.Range{ + StartPos: ast.Position{Offset: 14, Line: 2, Column: 13}, + EndPos: ast.Position{Offset: 15, Line: 2, Column: 14}, + }, + }, + FunctionBlock: &ast.FunctionBlock{ + Block: &ast.Block{ + Statements: []ast.Statement{ + &ast.IfStatement{ + Test: &ast.BoolExpression{ + Value: true, + Range: ast.Range{ + StartPos: ast.Position{Offset: 34, Line: 3, Column: 15}, + EndPos: ast.Position{Offset: 37, Line: 3, Column: 18}, + }, + }, + Then: &ast.Block{ + Statements: []ast.Statement{ + &ast.ReturnStatement{ + Expression: nil, + Range: ast.Range{ + StartPos: ast.Position{Offset: 57, Line: 4, Column: 16}, + EndPos: ast.Position{Offset: 62, Line: 4, Column: 21}, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 39, Line: 3, Column: 20}, + EndPos: ast.Position{Offset: 76, Line: 5, Column: 12}, + }, + }, + StartPos: ast.Position{Offset: 31, Line: 3, Column: 12}, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 17, Line: 2, Column: 16}, + EndPos: ast.Position{Offset: 86, Line: 6, Column: 8}, + }, + }, + }, + StartPos: ast.Position{Offset: 6, Line: 2, Column: 5}, + }, + }, + result.Declarations(), + ) +} + +func TestParseWhileStatementInFunctionDeclaration(t *testing.T) { + + t.Parallel() + + const code = ` + fun test() { + while true { + return + break + continue + } + } + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.FunctionDeclaration{ + Access: ast.AccessNotSpecified, + Identifier: ast.Identifier{ + Identifier: "test", + Pos: ast.Position{Offset: 10, Line: 2, Column: 9}, + }, + ParameterList: &ast.ParameterList{ + Range: ast.Range{ + StartPos: ast.Position{Offset: 14, Line: 2, Column: 13}, + EndPos: ast.Position{Offset: 15, Line: 2, Column: 14}, + }, + }, + FunctionBlock: &ast.FunctionBlock{ + Block: &ast.Block{ + Statements: []ast.Statement{ + &ast.WhileStatement{ + Test: &ast.BoolExpression{ + Value: true, + Range: ast.Range{ + StartPos: ast.Position{Offset: 37, Line: 3, Column: 18}, + EndPos: ast.Position{Offset: 40, Line: 3, Column: 21}, + }, + }, + Block: &ast.Block{ + Statements: []ast.Statement{ + &ast.ReturnStatement{ + Expression: nil, + Range: ast.Range{ + StartPos: ast.Position{Offset: 58, Line: 4, Column: 14}, + EndPos: ast.Position{Offset: 63, Line: 4, Column: 19}, + }, + }, + &ast.BreakStatement{ + Range: ast.Range{ + StartPos: ast.Position{Offset: 79, Line: 5, Column: 14}, + EndPos: ast.Position{Offset: 83, Line: 5, Column: 18}, + }, + }, + &ast.ContinueStatement{ + Range: ast.Range{ + StartPos: ast.Position{Offset: 99, Line: 6, Column: 14}, + EndPos: ast.Position{Offset: 106, Line: 6, Column: 21}, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 42, Line: 3, Column: 23}, + EndPos: ast.Position{Offset: 120, Line: 7, Column: 12}, + }, + }, + StartPos: ast.Position{Offset: 31, Line: 3, Column: 12}, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 17, Line: 2, Column: 16}, + EndPos: ast.Position{Offset: 130, Line: 8, Column: 8}, + }, + }, + }, + StartPos: ast.Position{Offset: 6, Line: 2, Column: 5}, + }, + }, + result.Declarations(), + ) +} + +func TestParseForStatementInFunctionDeclaration(t *testing.T) { + + t.Parallel() + + const code = ` + fun test() { + for x in xs {} + } + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.FunctionDeclaration{ + Access: ast.AccessNotSpecified, + Identifier: ast.Identifier{ + Identifier: "test", + Pos: ast.Position{Offset: 10, Line: 2, Column: 9}, + }, + ParameterList: &ast.ParameterList{ + Range: ast.Range{ + StartPos: ast.Position{Offset: 14, Line: 2, Column: 13}, + EndPos: ast.Position{Offset: 15, Line: 2, Column: 14}, + }, + }, + FunctionBlock: &ast.FunctionBlock{ + Block: &ast.Block{ + Statements: []ast.Statement{ + &ast.ForStatement{ + Identifier: ast.Identifier{ + Identifier: "x", + Pos: ast.Position{Offset: 35, Line: 3, Column: 16}, + }, + Value: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "xs", + Pos: ast.Position{Offset: 40, Line: 3, Column: 21}, + }, + }, + Block: &ast.Block{ + Range: ast.Range{ + StartPos: ast.Position{Offset: 43, Line: 3, Column: 24}, + EndPos: ast.Position{Offset: 44, Line: 3, Column: 25}, + }, + }, + StartPos: ast.Position{Offset: 31, Line: 3, Column: 12}, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 17, Line: 2, Column: 16}, + EndPos: ast.Position{Offset: 54, Line: 4, Column: 8}, + }, + }, + }, + StartPos: ast.Position{Offset: 6, Line: 2, Column: 5}, + }, + }, + result.Declarations(), + ) +} + +func TestParseAssignment(t *testing.T) { + + t.Parallel() + + const code = ` + fun test() { + a = 1 + } + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.FunctionDeclaration{ + Access: ast.AccessNotSpecified, + Identifier: ast.Identifier{ + Identifier: "test", + Pos: ast.Position{Offset: 10, Line: 2, Column: 9}, + }, + ParameterList: &ast.ParameterList{ + Range: ast.Range{ + StartPos: ast.Position{Offset: 14, Line: 2, Column: 13}, + EndPos: ast.Position{Offset: 15, Line: 2, Column: 14}, + }, + }, + FunctionBlock: &ast.FunctionBlock{ + Block: &ast.Block{ + Statements: []ast.Statement{ + &ast.AssignmentStatement{ + Target: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "a", + Pos: ast.Position{Offset: 31, Line: 3, Column: 12}, + }, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationCopy, + Pos: ast.Position{Offset: 33, Line: 3, Column: 14}, + }, + Value: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 35, Line: 3, Column: 16}, + EndPos: ast.Position{Offset: 35, Line: 3, Column: 16}, + }, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 17, Line: 2, Column: 16}, + EndPos: ast.Position{Offset: 45, Line: 4, Column: 8}, + }, + }, + }, + StartPos: ast.Position{Offset: 6, Line: 2, Column: 5}, + }, + }, + result.Declarations(), + ) +} + +func TestParseAccessAssignment(t *testing.T) { + + t.Parallel() + + const code = ` + fun test() { + x.foo.bar[0][1].baz = 1 + } + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.FunctionDeclaration{ + Access: ast.AccessNotSpecified, + Identifier: ast.Identifier{ + Identifier: "test", + Pos: ast.Position{Offset: 10, Line: 2, Column: 9}, + }, + ParameterList: &ast.ParameterList{ + Range: ast.Range{ + StartPos: ast.Position{Offset: 14, Line: 2, Column: 13}, + EndPos: ast.Position{Offset: 15, Line: 2, Column: 14}, + }, + }, + FunctionBlock: &ast.FunctionBlock{ + Block: &ast.Block{ + Statements: []ast.Statement{ + &ast.AssignmentStatement{ + Target: &ast.MemberExpression{ + Expression: &ast.IndexExpression{ + TargetExpression: &ast.IndexExpression{ + TargetExpression: &ast.MemberExpression{ + Expression: &ast.MemberExpression{ + Expression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "x", + Pos: ast.Position{Offset: 31, Line: 3, Column: 12}, + }, + }, + AccessPos: ast.Position{Offset: 32, Line: 3, Column: 13}, + Identifier: ast.Identifier{ + Identifier: "foo", + Pos: ast.Position{Offset: 33, Line: 3, Column: 14}, + }, + }, + AccessPos: ast.Position{Offset: 36, Line: 3, Column: 17}, + Identifier: ast.Identifier{ + Identifier: "bar", + Pos: ast.Position{Offset: 37, Line: 3, Column: 18}, + }, + }, + IndexingExpression: &ast.IntegerExpression{ + PositiveLiteral: []byte("0"), + Value: new(big.Int), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 41, Line: 3, Column: 22}, + EndPos: ast.Position{Offset: 41, Line: 3, Column: 22}, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 40, Line: 3, Column: 21}, + EndPos: ast.Position{Offset: 42, Line: 3, Column: 23}, + }, + }, + IndexingExpression: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 44, Line: 3, Column: 25}, + EndPos: ast.Position{Offset: 44, Line: 3, Column: 25}, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 43, Line: 3, Column: 24}, + EndPos: ast.Position{Offset: 45, Line: 3, Column: 26}, + }, + }, + AccessPos: ast.Position{Offset: 46, Line: 3, Column: 27}, + Identifier: ast.Identifier{ + Identifier: "baz", + Pos: ast.Position{Offset: 47, Line: 3, Column: 28}, + }, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationCopy, + Pos: ast.Position{Offset: 51, Line: 3, Column: 32}, + }, + Value: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 53, Line: 3, Column: 34}, + EndPos: ast.Position{Offset: 53, Line: 3, Column: 34}, + }, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 17, Line: 2, Column: 16}, + EndPos: ast.Position{Offset: 63, Line: 4, Column: 8}, + }, + }, + }, + StartPos: ast.Position{Offset: 6, Line: 2, Column: 5}, + }, + }, + result.Declarations(), + ) +} + +func TestParseExpressionStatementWithAccess(t *testing.T) { + + t.Parallel() + + const code = ` + fun test() { x.foo.bar[0][1].baz } + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.FunctionDeclaration{ + Access: ast.AccessNotSpecified, + Identifier: ast.Identifier{ + Identifier: "test", + Pos: ast.Position{Offset: 10, Line: 2, Column: 9}, + }, + ParameterList: &ast.ParameterList{ + Range: ast.Range{ + StartPos: ast.Position{Offset: 14, Line: 2, Column: 13}, + EndPos: ast.Position{Offset: 15, Line: 2, Column: 14}, + }, + }, + FunctionBlock: &ast.FunctionBlock{ + Block: &ast.Block{ + Statements: []ast.Statement{ + &ast.ExpressionStatement{ + Expression: &ast.MemberExpression{ + Expression: &ast.IndexExpression{ + TargetExpression: &ast.IndexExpression{ + TargetExpression: &ast.MemberExpression{ + Expression: &ast.MemberExpression{ + Expression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "x", + Pos: ast.Position{Offset: 19, Line: 2, Column: 18}, + }, + }, + AccessPos: ast.Position{Offset: 20, Line: 2, Column: 19}, + Identifier: ast.Identifier{ + Identifier: "foo", + Pos: ast.Position{Offset: 21, Line: 2, Column: 20}, + }, + }, + AccessPos: ast.Position{Offset: 24, Line: 2, Column: 23}, + Identifier: ast.Identifier{ + Identifier: "bar", + Pos: ast.Position{Offset: 25, Line: 2, Column: 24}, + }, + }, + IndexingExpression: &ast.IntegerExpression{ + PositiveLiteral: []byte("0"), + Value: new(big.Int), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 29, Line: 2, Column: 28}, + EndPos: ast.Position{Offset: 29, Line: 2, Column: 28}, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 28, Line: 2, Column: 27}, + EndPos: ast.Position{Offset: 30, Line: 2, Column: 29}, + }, + }, + IndexingExpression: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 32, Line: 2, Column: 31}, + EndPos: ast.Position{Offset: 32, Line: 2, Column: 31}, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 31, Line: 2, Column: 30}, + EndPos: ast.Position{Offset: 33, Line: 2, Column: 32}, + }, + }, + AccessPos: ast.Position{Offset: 34, Line: 2, Column: 33}, + Identifier: ast.Identifier{ + Identifier: "baz", + Pos: ast.Position{Offset: 35, Line: 2, Column: 34}, + }, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 17, Line: 2, Column: 16}, + EndPos: ast.Position{Offset: 39, Line: 2, Column: 38}, + }, + }, + }, + StartPos: ast.Position{Offset: 6, Line: 2, Column: 5}, + }, + }, + result.Declarations(), + ) +} + +func TestParseMoveStatement(t *testing.T) { + + t.Parallel() + + const code = ` + fun test() { + x <- y + } + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.FunctionDeclaration{ + Access: ast.AccessNotSpecified, + Identifier: ast.Identifier{ + Identifier: "test", + Pos: ast.Position{Offset: 13, Line: 2, Column: 12}, + }, + ParameterList: &ast.ParameterList{ + Range: ast.Range{ + StartPos: ast.Position{Offset: 17, Line: 2, Column: 16}, + EndPos: ast.Position{Offset: 18, Line: 2, Column: 17}, + }, + }, + FunctionBlock: &ast.FunctionBlock{ + Block: &ast.Block{ + Statements: []ast.Statement{ + &ast.AssignmentStatement{ + Target: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "x", + Pos: ast.Position{Offset: 34, Line: 3, Column: 12}, + }, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationMove, + Pos: ast.Position{Offset: 36, Line: 3, Column: 14}, + }, + Value: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "y", + Pos: ast.Position{Offset: 39, Line: 3, Column: 17}, + }, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 20, Line: 2, Column: 19}, + EndPos: ast.Position{Offset: 49, Line: 4, Column: 8}, + }, + }, + }, + StartPos: ast.Position{Offset: 9, Line: 2, Column: 8}, + }, + }, + result.Declarations(), + ) +} + +func TestParseFunctionExpressionStatementAfterVariableDeclarationWithCreateExpression(t *testing.T) { + + t.Parallel() + + const code = ` + fun test() { + let r <- create R() + (fun () {})() + } + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.FunctionDeclaration{ + Access: ast.AccessNotSpecified, + Identifier: ast.Identifier{ + Identifier: "test", + Pos: ast.Position{Offset: 11, Line: 2, Column: 10}, + }, + ParameterList: &ast.ParameterList{ + Range: ast.Range{ + StartPos: ast.Position{Offset: 15, Line: 2, Column: 14}, + EndPos: ast.Position{Offset: 16, Line: 2, Column: 15}, + }, + }, + FunctionBlock: &ast.FunctionBlock{ + Block: &ast.Block{ + Statements: []ast.Statement{ + &ast.VariableDeclaration{ + Access: ast.AccessNotSpecified, + IsConstant: true, + Identifier: ast.Identifier{ + Identifier: "r", + Pos: ast.Position{Offset: 34, Line: 3, Column: 14}, + }, + TypeAnnotation: nil, + Value: &ast.CreateExpression{ + InvocationExpression: &ast.InvocationExpression{ + InvokedExpression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "R", + Pos: ast.Position{Offset: 46, Line: 3, Column: 26}, + }, + }, + Arguments: nil, + ArgumentsStartPos: ast.Position{Offset: 47, Line: 3, Column: 27}, + EndPos: ast.Position{Offset: 48, Line: 3, Column: 28}, + }, + StartPos: ast.Position{Offset: 39, Line: 3, Column: 19}, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationMove, + Pos: ast.Position{Offset: 36, Line: 3, Column: 16}, + }, + StartPos: ast.Position{Offset: 30, Line: 3, Column: 10}, + }, + &ast.ExpressionStatement{ + Expression: &ast.InvocationExpression{ + InvokedExpression: &ast.FunctionExpression{ + ParameterList: &ast.ParameterList{ + Range: ast.Range{ + StartPos: ast.Position{Offset: 65, Line: 4, Column: 15}, + EndPos: ast.Position{Offset: 66, Line: 4, Column: 16}, + }, + }, + FunctionBlock: &ast.FunctionBlock{ + Block: &ast.Block{ + Statements: nil, + Range: ast.Range{ + StartPos: ast.Position{Offset: 68, Line: 4, Column: 18}, + EndPos: ast.Position{Offset: 69, Line: 4, Column: 19}, + }, + }, + PreConditions: nil, + PostConditions: nil, + }, + StartPos: ast.Position{Offset: 61, Line: 4, Column: 11}, + }, + Arguments: nil, + ArgumentsStartPos: ast.Position{Offset: 71, Line: 4, Column: 21}, + EndPos: ast.Position{Offset: 72, Line: 4, Column: 22}, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 18, Line: 2, Column: 17}, + EndPos: ast.Position{Offset: 80, Line: 5, Column: 6}, + }, + }, + PreConditions: nil, + PostConditions: nil, + }, + StartPos: ast.Position{Offset: 7, Line: 2, Column: 6}, + }, + }, + result.Declarations(), + ) +} + +// TestParseExpressionStatementAfterReturnStatement tests that a return statement +// does *not* consume an expression from the next statement as the return value +func TestParseExpressionStatementAfterReturnStatement(t *testing.T) { + + t.Parallel() + + const code = ` + fun test() { + return + destroy x + } + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.FunctionDeclaration{ + Access: ast.AccessNotSpecified, + Identifier: ast.Identifier{ + Identifier: "test", + Pos: ast.Position{Offset: 11, Line: 2, Column: 10}, + }, + ParameterList: &ast.ParameterList{ + Range: ast.Range{ + StartPos: ast.Position{Offset: 15, Line: 2, Column: 14}, + EndPos: ast.Position{Offset: 16, Line: 2, Column: 15}, + }, + }, + FunctionBlock: &ast.FunctionBlock{ + Block: &ast.Block{ + Statements: []ast.Statement{ + &ast.ReturnStatement{ + Expression: nil, + Range: ast.Range{ + StartPos: ast.Position{Offset: 30, Line: 3, Column: 10}, + EndPos: ast.Position{Offset: 35, Line: 3, Column: 15}, + }, + }, + &ast.ExpressionStatement{ + Expression: &ast.DestroyExpression{ + Expression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "x", + Pos: ast.Position{Offset: 55, Line: 4, Column: 18}, + }, + }, + StartPos: ast.Position{Offset: 47, Line: 4, Column: 10}, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 18, Line: 2, Column: 17}, + EndPos: ast.Position{Offset: 63, Line: 5, Column: 6}, + }, + }, + }, + StartPos: ast.Position{Offset: 7, Line: 2, Column: 6}, + }, + }, + result.Declarations(), + ) +} + +func TestParseSwapStatementInFunctionDeclaration(t *testing.T) { + + t.Parallel() + + const code = ` + fun test() { + foo[0] <-> bar.baz + } + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.FunctionDeclaration{ + Access: ast.AccessNotSpecified, + Identifier: ast.Identifier{ + Identifier: "test", + Pos: ast.Position{Offset: 11, Line: 2, Column: 10}, + }, + ParameterList: &ast.ParameterList{ + Range: ast.Range{ + StartPos: ast.Position{Offset: 15, Line: 2, Column: 14}, + EndPos: ast.Position{Offset: 16, Line: 2, Column: 15}, + }, + }, + FunctionBlock: &ast.FunctionBlock{ + Block: &ast.Block{ + Statements: []ast.Statement{ + &ast.SwapStatement{ + Left: &ast.IndexExpression{ + TargetExpression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "foo", + Pos: ast.Position{Offset: 30, Line: 3, Column: 10}, + }, + }, + IndexingExpression: &ast.IntegerExpression{ + PositiveLiteral: []byte("0"), + Value: new(big.Int), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 34, Line: 3, Column: 14}, + EndPos: ast.Position{Offset: 34, Line: 3, Column: 14}, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 33, Line: 3, Column: 13}, + EndPos: ast.Position{Offset: 35, Line: 3, Column: 15}, + }, + }, + Right: &ast.MemberExpression{ + Expression: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "bar", + Pos: ast.Position{Offset: 41, Line: 3, Column: 21}, + }, + }, + AccessPos: ast.Position{Offset: 44, Line: 3, Column: 24}, + Identifier: ast.Identifier{ + Identifier: "baz", + Pos: ast.Position{Offset: 45, Line: 3, Column: 25}, + }, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 18, Line: 2, Column: 17}, + EndPos: ast.Position{Offset: 55, Line: 4, Column: 6}, + }, + }, + PreConditions: nil, + PostConditions: nil, + }, + StartPos: ast.Position{Offset: 7, Line: 2, Column: 6}, + }, + }, + result.Declarations(), + ) +} + +func TestParseStatementsWithWhitespace(t *testing.T) { + + t.Parallel() + + t.Run("two statements: variable declaration and parenthesized expression, not one function-call", func(t *testing.T) { + t.Parallel() + + const code = ` + a == b + (c) + ` + + statements, errs := testParseStatements(code) + require.Empty(t, errs) + + require.Len(t, statements, 2) + }) + + t.Run("two statements: binary expression and array literal, not an indexing expression", func(t *testing.T) { + t.Parallel() + + const code = ` + a == b + [c] + ` + + statements, errs := testParseStatements(code) + require.Empty(t, errs) + + require.Len(t, statements, 2) + }) + + t.Run("two statements: binary expression and unary prefix negation, not unary postfix force", func(t *testing.T) { + t.Parallel() + + const code = ` + a == b + !c == d + ` + + statements, errs := testParseStatements(code) + require.Empty(t, errs) + + require.Len(t, statements, 2) + }) + + t.Run("one statement: binary expression, right-hand side with member access", func(t *testing.T) { + t.Parallel() + + const code = ` + a == b + .c + ` + + statements, errs := testParseStatements(code) + require.Empty(t, errs) + + require.Len(t, statements, 1) + }) +} diff --git a/runtime/old_parser/transaction.go b/runtime/old_parser/transaction.go new file mode 100644 index 0000000000..d2cb79d62d --- /dev/null +++ b/runtime/old_parser/transaction.go @@ -0,0 +1,292 @@ +/* + * Cadence - The resource-oriented smart contract programming language + * + * Copyright Dapper Labs, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package old_parser + +import ( + "github.com/onflow/cadence/runtime/ast" + "github.com/onflow/cadence/runtime/common" + "github.com/onflow/cadence/runtime/parser/lexer" +) + +// parseTransactionDeclaration parses a transaction declaration. +// +// transactionDeclaration : 'transaction' +// parameterList? +// '{' +// fields +// prepare? +// preConditions? +// ( execute +// | execute postConditions +// | postConditions +// | postConditions execute +// | /* no execute or postConditions */ +// ) +// '}' +func parseTransactionDeclaration(p *parser, docString string) (*ast.TransactionDeclaration, error) { + + startPos := p.current.StartPos + + // Skip the `transaction` keyword + p.nextSemanticToken() + + // Parameter list (optional) + + var parameterList *ast.ParameterList + var err error + + if p.current.Is(lexer.TokenParenOpen) { + parameterList, err = parseParameterList(p) + if err != nil { + return nil, err + } + } + + p.skipSpaceAndComments() + _, err = p.mustOne(lexer.TokenBraceOpen) + if err != nil { + return nil, err + } + + // Fields + + fields, err := parseTransactionFields(p) + if err != nil { + return nil, err + } + + // Prepare (optional) or execute (optional) + + var prepare *ast.SpecialFunctionDeclaration + var execute *ast.SpecialFunctionDeclaration + + p.skipSpaceAndComments() + if p.current.Is(lexer.TokenIdentifier) { + + keyword := p.currentTokenSource() + + switch string(keyword) { + case keywordPrepare: + identifier := p.tokenToIdentifier(p.current) + // Skip the `prepare` keyword + p.next() + prepare, err = parseSpecialFunctionDeclaration( + p, + false, + ast.AccessNotSpecified, + nil, + nil, + nil, + identifier, + "", + ) + if err != nil { + return nil, err + } + + case keywordExecute: + execute, err = parseTransactionExecute(p) + if err != nil { + return nil, err + } + + default: + return nil, p.syntaxError( + "unexpected identifier, expected keyword %q or %q, got %q", + keywordPrepare, + keywordExecute, + keyword, + ) + } + } + + // Pre-conditions (optional) + + var preConditions *ast.Conditions + + if execute == nil { + p.skipSpaceAndComments() + if p.isToken(p.current, lexer.TokenIdentifier, keywordPre) { + // Skip the `pre` keyword + p.next() + conditions, err := parseConditions(p, ast.ConditionKindPre) + if err != nil { + return nil, err + } + + preConditions = &conditions + } + } + + // Execute / post-conditions (both optional, in any order) + + var postConditions *ast.Conditions + + var endPos ast.Position + + sawPost := false + atEnd := false + for !atEnd { + p.skipSpaceAndComments() + + switch p.current.Type { + case lexer.TokenIdentifier: + + keyword := p.currentTokenSource() + switch string(keyword) { + case keywordExecute: + if execute != nil { + return nil, p.syntaxError("unexpected second %q block", keywordExecute) + } + + execute, err = parseTransactionExecute(p) + if err != nil { + return nil, err + } + + case keywordPost: + if sawPost { + return nil, p.syntaxError("unexpected second post-conditions") + } + // Skip the `post` keyword + p.next() + conditions, err := parseConditions(p, ast.ConditionKindPost) + if err != nil { + return nil, err + } + + postConditions = &conditions + sawPost = true + + default: + return nil, p.syntaxError( + "unexpected identifier, expected keyword %q or %q, got %q", + keywordExecute, + keywordPost, + keyword, + ) + } + + case lexer.TokenBraceClose: + endPos = p.current.EndPos + // Skip the closing brace + p.next() + atEnd = true + + default: + return nil, p.syntaxError("unexpected token: %s", p.current.Type) + } + } + + return ast.NewTransactionDeclaration( + p.memoryGauge, + parameterList, + fields, + prepare, + preConditions, + postConditions, + execute, + docString, + ast.NewRange( + p.memoryGauge, + startPos, + endPos, + ), + ), nil +} + +func parseTransactionFields(p *parser) (fields []*ast.FieldDeclaration, err error) { + for { + _, docString := p.parseTrivia(triviaOptions{ + skipNewlines: true, + parseDocStrings: true, + }) + + switch p.current.Type { + case lexer.TokenSemicolon: + // Skip the semicolon + p.next() + continue + + case lexer.TokenBraceClose, lexer.TokenEOF: + return + + case lexer.TokenIdentifier: + switch string(p.currentTokenSource()) { + case keywordLet, keywordVar: + field, err := parseFieldWithVariableKind( + p, + ast.AccessNotSpecified, + nil, + nil, + nil, + docString, + ) + if err != nil { + return nil, err + } + + fields = append(fields, field) + continue + + default: + return + } + + default: + return + } + } +} + +func parseTransactionExecute(p *parser) (*ast.SpecialFunctionDeclaration, error) { + identifier := p.tokenToIdentifier(p.current) + + // Skip the `execute` keyword + p.nextSemanticToken() + + block, err := parseBlock(p) + if err != nil { + return nil, err + } + + return ast.NewSpecialFunctionDeclaration( + p.memoryGauge, + common.DeclarationKindExecute, + ast.NewFunctionDeclaration( + p.memoryGauge, + ast.AccessNotSpecified, + ast.FunctionPurityUnspecified, + false, + false, + identifier, + nil, + nil, + nil, + ast.NewFunctionBlock( + p.memoryGauge, + block, + nil, + nil, + ), + identifier.Pos, + "", + ), + ), nil +} diff --git a/runtime/old_parser/type.go b/runtime/old_parser/type.go new file mode 100644 index 0000000000..ba451cc861 --- /dev/null +++ b/runtime/old_parser/type.go @@ -0,0 +1,988 @@ +/* + * Cadence - The resource-oriented smart contract programming language + * + * Copyright Dapper Labs, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package old_parser + +import ( + "github.com/onflow/cadence/runtime/ast" + "github.com/onflow/cadence/runtime/errors" + "github.com/onflow/cadence/runtime/parser/lexer" +) + +const ( + typeLeftBindingPowerOptional = 10 * (iota + 1) + typeLeftBindingPowerReference + typeLeftBindingPowerRestriction + typeLeftBindingPowerInstantiation +) + +type typeNullDenotationFunc func(parser *parser, token lexer.Token) (ast.Type, error) + +var typeNullDenotations [lexer.TokenMax]typeNullDenotationFunc + +type typeLeftDenotationFunc func(parser *parser, token lexer.Token, left ast.Type) (ast.Type, error) +type typeMetaLeftDenotationFunc func( + p *parser, + rightBindingPower int, + left ast.Type, +) ( + result ast.Type, + err error, + done bool, +) + +var typeLeftBindingPowers [lexer.TokenMax]int +var typeLeftDenotations [lexer.TokenMax]typeLeftDenotationFunc +var typeMetaLeftDenotations [lexer.TokenMax]typeMetaLeftDenotationFunc + +func setTypeNullDenotation(tokenType lexer.TokenType, nullDenotation typeNullDenotationFunc) { + current := typeNullDenotations[tokenType] + if current != nil { + panic(errors.NewUnexpectedError( + "type null denotation for token %s already exists", + tokenType, + )) + } + typeNullDenotations[tokenType] = nullDenotation +} + +func setTypeLeftBindingPower(tokenType lexer.TokenType, power int) { + current := typeLeftBindingPowers[tokenType] + if current > power { + return + } + typeLeftBindingPowers[tokenType] = power +} + +func setTypeLeftDenotation(tokenType lexer.TokenType, leftDenotation typeLeftDenotationFunc) { + current := typeLeftDenotations[tokenType] + if current != nil { + panic(errors.NewUnexpectedError( + "type left denotation for token %s already exists", + tokenType, + )) + } + typeLeftDenotations[tokenType] = leftDenotation +} + +func setTypeMetaLeftDenotation(tokenType lexer.TokenType, metaLeftDenotation typeMetaLeftDenotationFunc) { + current := typeMetaLeftDenotations[tokenType] + if current != nil { + panic(errors.NewUnexpectedError( + "type meta left denotation for token %s already exists", + tokenType, + )) + } + typeMetaLeftDenotations[tokenType] = metaLeftDenotation +} + +type prefixTypeFunc func(parser *parser, right ast.Type, tokenRange ast.Range) ast.Type +type postfixTypeFunc func(parser *parser, left ast.Type, tokenRange ast.Range) ast.Type + +type literalType struct { + nullDenotation typeNullDenotationFunc + tokenType lexer.TokenType +} + +type prefixType struct { + nullDenotation prefixTypeFunc + bindingPower int + tokenType lexer.TokenType +} + +type postfixType struct { + leftDenotation postfixTypeFunc + bindingPower int + tokenType lexer.TokenType +} + +func defineType(def any) { + switch def := def.(type) { + case prefixType: + tokenType := def.tokenType + setTypeNullDenotation( + tokenType, + func(parser *parser, token lexer.Token) (ast.Type, error) { + right, err := parseType(parser, def.bindingPower) + if err != nil { + return nil, err + } + + return def.nullDenotation(parser, right, token.Range), nil + }, + ) + case postfixType: + tokenType := def.tokenType + setTypeLeftBindingPower(tokenType, def.bindingPower) + setTypeLeftDenotation( + tokenType, + func(p *parser, token lexer.Token, left ast.Type) (ast.Type, error) { + return def.leftDenotation(p, left, token.Range), nil + }, + ) + case literalType: + tokenType := def.tokenType + setTypeNullDenotation(tokenType, def.nullDenotation) + default: + panic(errors.NewUnreachableError()) + } +} + +func init() { + defineArrayType() + defineOptionalType() + defineReferenceType() + defineRestrictedOrDictionaryType() + defineFunctionType() + defineInstantiationType() + + setTypeNullDenotation( + lexer.TokenIdentifier, + func(p *parser, token lexer.Token) (ast.Type, error) { + + switch string(p.tokenSource(token)) { + case keywordAuth: + p.skipSpaceAndComments() + + _, err := p.mustOne(lexer.TokenAmpersand) + if err != nil { + return nil, err + } + + right, err := parseType(p, typeLeftBindingPowerReference) + if err != nil { + return nil, err + } + + return ast.NewReferenceType( + p.memoryGauge, + nil, + right, + token.StartPos, + ), nil + + default: + return parseNominalTypeRemainder(p, token) + } + }, + ) +} + +func parseNominalTypeRemainder(p *parser, token lexer.Token) (*ast.NominalType, error) { + var nestedIdentifiers []ast.Identifier + + for p.current.Is(lexer.TokenDot) { + // Skip the dot + p.next() + + nestedToken := p.current + + if !nestedToken.Is(lexer.TokenIdentifier) { + return nil, p.syntaxError( + "expected identifier after %s, got %s", + lexer.TokenDot, + nestedToken.Type, + ) + } + + nestedIdentifier := p.tokenToIdentifier(nestedToken) + + // Skip the identifier + p.next() + + nestedIdentifiers = append( + nestedIdentifiers, + nestedIdentifier, + ) + + } + + return ast.NewNominalType( + p.memoryGauge, + p.tokenToIdentifier(token), + nestedIdentifiers, + ), nil +} + +func defineArrayType() { + setTypeNullDenotation( + lexer.TokenBracketOpen, + func(p *parser, startToken lexer.Token) (ast.Type, error) { + + elementType, err := parseType(p, lowestBindingPower) + if err != nil { + return nil, err + } + + p.skipSpaceAndComments() + + var size *ast.IntegerExpression + + if p.current.Is(lexer.TokenSemicolon) { + // Skip the semicolon + p.nextSemanticToken() + + if !p.current.Type.IsIntegerLiteral() { + p.reportSyntaxError("expected positive integer size for constant sized type") + + // Skip the invalid non-integer literal token + p.next() + + } else { + numberExpression, err := parseExpression(p, lowestBindingPower) + if err != nil { + return nil, err + } + + integerExpression, ok := numberExpression.(*ast.IntegerExpression) + if !ok || integerExpression.Value.Sign() < 0 { + p.reportSyntaxError("expected positive integer size for constant sized type") + } else { + size = integerExpression + } + } + } + + p.skipSpaceAndComments() + + endToken, err := p.mustOne(lexer.TokenBracketClose) + if err != nil { + return nil, err + } + + typeRange := ast.NewRange( + p.memoryGauge, + startToken.StartPos, + endToken.EndPos, + ) + + if size != nil { + return ast.NewConstantSizedType( + p.memoryGauge, + elementType, + size, + typeRange, + ), nil + } else { + return ast.NewVariableSizedType( + p.memoryGauge, + elementType, + typeRange, + ), nil + } + }, + ) +} + +func defineOptionalType() { + defineType(postfixType{ + tokenType: lexer.TokenQuestionMark, + bindingPower: typeLeftBindingPowerOptional, + leftDenotation: func(p *parser, left ast.Type, tokenRange ast.Range) ast.Type { + return ast.NewOptionalType( + p.memoryGauge, + left, + tokenRange.EndPos, + ) + }, + }) + + defineType(postfixType{ + tokenType: lexer.TokenDoubleQuestionMark, + bindingPower: typeLeftBindingPowerOptional, + leftDenotation: func(p *parser, left ast.Type, tokenRange ast.Range) ast.Type { + return ast.NewOptionalType( + p.memoryGauge, + ast.NewOptionalType( + p.memoryGauge, + left, + tokenRange.StartPos, + ), + tokenRange.EndPos, + ) + }, + }) +} + +func defineReferenceType() { + defineType(prefixType{ + tokenType: lexer.TokenAmpersand, + bindingPower: typeLeftBindingPowerReference, + nullDenotation: func(p *parser, right ast.Type, tokenRange ast.Range) ast.Type { + return ast.NewReferenceType( + p.memoryGauge, + nil, + right, + tokenRange.StartPos, + ) + }, + }) +} + +func defineRestrictedOrDictionaryType() { + + // For the null denotation it is not clear after the start + // if it is a restricted type or a dictionary type. + // + // If a colon is seen it is a dictionary type. + // If no colon is seen it is a restricted type. + + setTypeNullDenotation( + lexer.TokenBraceOpen, + func(p *parser, startToken lexer.Token) (ast.Type, error) { + + var endPos ast.Position + + var dictionaryType *ast.DictionaryType + var restrictedType *ast.IntersectionType + + var firstType ast.Type + + atEnd := false + + expectType := true + + for !atEnd { + p.skipSpaceAndComments() + + switch p.current.Type { + case lexer.TokenComma: + if dictionaryType != nil { + return nil, p.syntaxError("unexpected comma in dictionary type") + } + if expectType { + return nil, p.syntaxError("unexpected comma in restricted type") + } + if restrictedType == nil { + firstNominalType, ok := firstType.(*ast.NominalType) + if !ok { + return nil, p.syntaxError("non-nominal type in restriction list: %s", firstType) + } + restrictedType = ast.NewIntersectionType( + p.memoryGauge, + []*ast.NominalType{ + firstNominalType, + }, + ast.NewRange( + p.memoryGauge, + startToken.StartPos, + ast.EmptyPosition, + ), + ) + } + // Skip the comma + p.next() + expectType = true + + case lexer.TokenColon: + if restrictedType != nil { + return nil, p.syntaxError("unexpected colon in restricted type") + } + if expectType { + return nil, p.syntaxError("unexpected colon in dictionary type") + } + if dictionaryType == nil { + if firstType == nil { + return nil, p.syntaxError("unexpected colon after missing dictionary key type") + } + dictionaryType = ast.NewDictionaryType( + p.memoryGauge, + firstType, + nil, + ast.NewRange( + p.memoryGauge, + startToken.StartPos, + ast.EmptyPosition, + ), + ) + } else { + return nil, p.syntaxError("unexpected colon in dictionary type") + } + // Skip the colon + p.next() + expectType = true + + case lexer.TokenBraceClose: + if expectType { + switch { + case dictionaryType != nil: + p.reportSyntaxError("missing dictionary value type") + case restrictedType != nil: + p.reportSyntaxError("missing type after comma") + } + } + endPos = p.current.EndPos + // Skip the closing brace + p.next() + atEnd = true + + case lexer.TokenEOF: + if expectType { + return nil, p.syntaxError("invalid end of input, expected type") + } else { + return nil, p.syntaxError("invalid end of input, expected %s", lexer.TokenBraceClose) + } + + default: + if !expectType { + return nil, p.syntaxError("unexpected type") + } + + ty, err := parseType(p, lowestBindingPower) + if err != nil { + return nil, err + } + + expectType = false + + switch { + case dictionaryType != nil: + dictionaryType.ValueType = ty + + case restrictedType != nil: + nominalType, ok := ty.(*ast.NominalType) + if !ok { + return nil, p.syntaxError("non-nominal type in restriction list: %s", ty) + } + restrictedType.Types = append(restrictedType.Types, nominalType) + + default: + firstType = ty + } + } + } + + switch { + case restrictedType != nil: + restrictedType.EndPos = endPos + return restrictedType, nil + case dictionaryType != nil: + dictionaryType.EndPos = endPos + return dictionaryType, nil + default: + restrictedType = ast.NewIntersectionType( + p.memoryGauge, + nil, + ast.NewRange( + p.memoryGauge, + startToken.StartPos, + endPos, + ), + ) + if firstType != nil { + firstNominalType, ok := firstType.(*ast.NominalType) + if !ok { + return nil, p.syntaxError("non-nominal type in restriction list: %s", firstType) + } + restrictedType.Types = append(restrictedType.Types, firstNominalType) + } + return restrictedType, nil + } + }, + ) + + // For the left denotation we need a meta left denotation: + // We need to look ahead and check if the brace is followed by whitespace or not. + // In case there is a space, the type is *not* considered a restricted type. + // This handles the ambiguous case where a function return type's open brace + // may either be a restricted type (if there is no whitespace) + // or the start of the function body (if there is whitespace). + + setTypeMetaLeftDenotation( + lexer.TokenBraceOpen, + func(p *parser, rightBindingPower int, left ast.Type) (result ast.Type, err error, done bool) { + + // Perform a lookahead + + current := p.current + cursor := p.tokens.Cursor() + + // Skip the `{` token. + p.next() + + // In case there is a space, the type is *not* considered a restricted type. + // The buffered tokens are replayed to allow them to be re-parsed. + + if p.current.Is(lexer.TokenSpace) { + p.current = current + p.tokens.Revert(cursor) + + return left, nil, true + } + + // It was determined that a restricted type is parsed. + // Still, it should have maybe not been parsed if the right binding power + // was higher. In that case, replay the buffered tokens and stop. + + if rightBindingPower >= typeLeftBindingPowerRestriction { + p.current = current + p.tokens.Revert(cursor) + return left, nil, true + } + + nominalTypes, endPos, err := parseNominalTypes(p, lexer.TokenBraceClose) + if err != nil { + return nil, err, true + } + + // Skip the closing brace + p.next() + + result = ast.NewIntersectionType( + p.memoryGauge, + nominalTypes, + ast.NewRange( + p.memoryGauge, + left.StartPosition(), + endPos, + ), + ) + + return result, err, false + }, + ) +} + +// parseNominalTypes parses zero or more nominal types separated by comma. +func parseNominalTypes( + p *parser, + endTokenType lexer.TokenType, +) ( + nominalTypes []*ast.NominalType, + endPos ast.Position, + err error, +) { + expectType := true + atEnd := false + for !atEnd { + p.skipSpaceAndComments() + + switch p.current.Type { + case lexer.TokenComma: + if expectType { + return nil, ast.EmptyPosition, p.syntaxError("unexpected comma") + } + // Skip the comma + p.next() + expectType = true + + case endTokenType: + if expectType && len(nominalTypes) > 0 { + p.reportSyntaxError("missing type after comma") + } + endPos = p.current.EndPos + atEnd = true + + case lexer.TokenEOF: + if expectType { + return nil, ast.EmptyPosition, p.syntaxError("invalid end of input, expected type") + } else { + return nil, ast.EmptyPosition, p.syntaxError("invalid end of input, expected %s", endTokenType) + } + + default: + if !expectType { + return nil, ast.EmptyPosition, p.syntaxError( + "unexpected token: got %s, expected %s or %s", + p.current.Type, + lexer.TokenComma, + endTokenType, + ) + } + + ty, err := parseType(p, lowestBindingPower) + if err != nil { + return nil, ast.EmptyPosition, err + } + + expectType = false + + nominalType, ok := ty.(*ast.NominalType) + if !ok { + return nil, ast.EmptyPosition, p.syntaxError("unexpected non-nominal type: %s", ty) + } + nominalTypes = append(nominalTypes, nominalType) + } + } + + return +} + +func defineFunctionType() { + setTypeNullDenotation( + lexer.TokenParenOpen, + func(p *parser, startToken lexer.Token) (ast.Type, error) { + + parameterTypeAnnotations, err := parseParameterTypeAnnotations(p) + if err != nil { + return nil, err + } + + p.skipSpaceAndComments() + _, err = p.mustOne(lexer.TokenColon) + if err != nil { + return nil, err + } + + p.skipSpaceAndComments() + returnTypeAnnotation, err := parseTypeAnnotation(p) + if err != nil { + return nil, err + } + + p.skipSpaceAndComments() + endToken, err := p.mustOne(lexer.TokenParenClose) + if err != nil { + return nil, err + } + + return ast.NewFunctionType( + p.memoryGauge, + ast.FunctionPurityUnspecified, + parameterTypeAnnotations, + returnTypeAnnotation, + ast.NewRange( + p.memoryGauge, + startToken.StartPos, + endToken.EndPos, + ), + ), nil + }, + ) +} + +func parseParameterTypeAnnotations(p *parser) (typeAnnotations []*ast.TypeAnnotation, err error) { + + p.skipSpaceAndComments() + _, err = p.mustOne(lexer.TokenParenOpen) + if err != nil { + return + } + + expectTypeAnnotation := true + + atEnd := false + for !atEnd { + p.skipSpaceAndComments() + switch p.current.Type { + case lexer.TokenComma: + if expectTypeAnnotation { + return nil, p.syntaxError( + "expected type annotation or end of list, got %q", + p.current.Type, + ) + } + // Skip the comma + p.next() + expectTypeAnnotation = true + + case lexer.TokenParenClose: + // Skip the closing paren + p.next() + atEnd = true + + case lexer.TokenEOF: + return nil, p.syntaxError( + "missing %q at end of list", + lexer.TokenParenClose, + ) + + default: + if !expectTypeAnnotation { + return nil, p.syntaxError( + "expected comma or end of list, got %q", + p.current.Type, + ) + } + + typeAnnotation, err := parseTypeAnnotation(p) + if err != nil { + return nil, err + } + + typeAnnotations = append(typeAnnotations, typeAnnotation) + + expectTypeAnnotation = false + } + } + + return +} + +func parseType(p *parser, rightBindingPower int) (ast.Type, error) { + + if p.typeDepth == typeDepthLimit { + return nil, TypeDepthLimitReachedError{ + Pos: p.current.StartPos, + } + } + + p.typeDepth++ + defer func() { + p.typeDepth-- + }() + + p.skipSpaceAndComments() + t := p.current + p.next() + + left, err := applyTypeNullDenotation(p, t) + if err != nil { + return nil, err + } + + for { + var done bool + left, err, done = applyTypeMetaLeftDenotation(p, rightBindingPower, left) + if err != nil { + return nil, err + } + + if done { + break + } + } + + return left, nil +} + +func applyTypeMetaLeftDenotation( + p *parser, + rightBindingPower int, + left ast.Type, +) ( + result ast.Type, + err error, + done bool, +) { + // By default, left denotations are applied if the right binding power + // is less than the left binding power of the current token. + // + // Token-specific meta-left denotations allow customizing this, + // e.g. determining the left binding power based on parsing more tokens, + // or performing look-ahead + + metaLeftDenotation := typeMetaLeftDenotations[p.current.Type] + if metaLeftDenotation == nil { + metaLeftDenotation = defaultTypeMetaLeftDenotation + } + + return metaLeftDenotation(p, rightBindingPower, left) +} + +// defaultTypeMetaLeftDenotation is the default type left denotation, which applies +// if the right binding power is less than the left binding power of the current token +func defaultTypeMetaLeftDenotation( + p *parser, + rightBindingPower int, + left ast.Type, +) ( + result ast.Type, + err error, + done bool, +) { + if rightBindingPower >= typeLeftBindingPowers[p.current.Type] { + return left, nil, true + } + + t := p.current + + p.next() + + result, err = applyTypeLeftDenotation(p, t, left) + + return result, err, false +} + +func parseTypeAnnotation(p *parser) (*ast.TypeAnnotation, error) { + p.skipSpaceAndComments() + + startPos := p.current.StartPos + + isResource := false + if p.current.Is(lexer.TokenAt) { + // Skip the `@` + p.next() + isResource = true + } + + ty, err := parseType(p, lowestBindingPower) + if err != nil { + return nil, err + } + + return ast.NewTypeAnnotation( + p.memoryGauge, + isResource, + ty, + startPos, + ), nil +} + +func applyTypeNullDenotation(p *parser, token lexer.Token) (ast.Type, error) { + tokenType := token.Type + nullDenotation := typeNullDenotations[tokenType] + if nullDenotation == nil { + return nil, p.syntaxError("unexpected token in type: %s", tokenType) + } + return nullDenotation(p, token) +} + +func applyTypeLeftDenotation(p *parser, token lexer.Token, left ast.Type) (ast.Type, error) { + leftDenotation := typeLeftDenotations[token.Type] + if leftDenotation == nil { + return nil, p.syntaxError("unexpected token in type: %s", token.Type) + } + return leftDenotation(p, token, left) +} + +func parseNominalTypeInvocationRemainder(p *parser) (*ast.InvocationExpression, error) { + p.skipSpaceAndComments() + identifier, err := p.mustOne(lexer.TokenIdentifier) + if err != nil { + return nil, err + } + + ty, err := parseNominalTypeRemainder(p, identifier) + if err != nil { + return nil, err + } + + p.skipSpaceAndComments() + parenOpenToken, err := p.mustOne(lexer.TokenParenOpen) + if err != nil { + return nil, err + } + + argumentsStartPos := parenOpenToken.EndPos + arguments, endPos, err := parseArgumentListRemainder(p) + if err != nil { + return nil, err + } + + var invokedExpression ast.Expression = ast.NewIdentifierExpression( + p.memoryGauge, + ty.Identifier, + ) + + for _, nestedIdentifier := range ty.NestedIdentifiers { + invokedExpression = ast.NewMemberExpression( + p.memoryGauge, + invokedExpression, + false, + nestedIdentifier.Pos, + nestedIdentifier, + ) + } + + return ast.NewInvocationExpression( + p.memoryGauge, + invokedExpression, + nil, + arguments, + argumentsStartPos, + endPos, + ), nil +} + +// parseCommaSeparatedTypeAnnotations parses zero or more type annotations separated by comma. +func parseCommaSeparatedTypeAnnotations( + p *parser, + endTokenType lexer.TokenType, +) ( + typeAnnotations []*ast.TypeAnnotation, + err error, +) { + expectTypeAnnotation := true + atEnd := false + for !atEnd { + p.skipSpaceAndComments() + + switch p.current.Type { + case lexer.TokenComma: + if expectTypeAnnotation { + return nil, p.syntaxError("unexpected comma") + } + // Skip the comma + p.next() + expectTypeAnnotation = true + + case endTokenType: + if expectTypeAnnotation && len(typeAnnotations) > 0 { + p.reportSyntaxError("missing type annotation after comma") + } + atEnd = true + + case lexer.TokenEOF: + if expectTypeAnnotation { + return nil, p.syntaxError("invalid end of input, expected type") + } else { + return nil, p.syntaxError("invalid end of input, expected %s", endTokenType) + } + + default: + if !expectTypeAnnotation { + return nil, p.syntaxError( + "unexpected token: got %s, expected %s or %s", + p.current.Type, + lexer.TokenComma, + endTokenType, + ) + } + + typeAnnotation, err := parseTypeAnnotation(p) + if err != nil { + return nil, err + } + + typeAnnotations = append(typeAnnotations, typeAnnotation) + + expectTypeAnnotation = false + } + } + + return +} + +func defineInstantiationType() { + setTypeLeftBindingPower(lexer.TokenLess, typeLeftBindingPowerInstantiation) + setTypeLeftDenotation( + lexer.TokenLess, + func(p *parser, token lexer.Token, left ast.Type) (ast.Type, error) { + typeArgumentsStartPos := token.StartPos + + typeArguments, err := parseCommaSeparatedTypeAnnotations(p, lexer.TokenGreater) + if err != nil { + return nil, err + } + + endToken, err := p.mustOne(lexer.TokenGreater) + if err != nil { + return nil, err + } + + return ast.NewInstantiationType( + p.memoryGauge, + left, + typeArguments, + typeArgumentsStartPos, + endToken.EndPos, + ), nil + }, + ) +} diff --git a/runtime/old_parser/type_test.go b/runtime/old_parser/type_test.go new file mode 100644 index 0000000000..e83483bf49 --- /dev/null +++ b/runtime/old_parser/type_test.go @@ -0,0 +1,2839 @@ +/* + * Cadence - The resource-oriented smart contract programming language + * + * Copyright Dapper Labs, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package old_parser + +import ( + "math/big" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/onflow/cadence/runtime/ast" + "github.com/onflow/cadence/runtime/common" + "github.com/onflow/cadence/runtime/tests/utils" +) + +func TestParseNominalType(t *testing.T) { + + t.Parallel() + + t.Run("simple", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseType("Int") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Int", + Pos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + result, + ) + }) + + t.Run("nested", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseType("Foo.Bar") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Foo", + Pos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + NestedIdentifiers: []ast.Identifier{ + { + Identifier: "Bar", + Pos: ast.Position{Line: 1, Column: 4, Offset: 4}, + }, + }, + }, + result, + ) + }) +} + +func TestParseArrayType(t *testing.T) { + + t.Parallel() + + t.Run("variable", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseType("[Int]") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.VariableSizedType{ + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Int", + Pos: ast.Position{Line: 1, Column: 1, Offset: 1}, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + }, + }, + result, + ) + }) + + t.Run("constant", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseType("[Int ; 2 ]") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.ConstantSizedType{ + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Int", + Pos: ast.Position{Line: 1, Column: 1, Offset: 1}, + }, + }, + Size: &ast.IntegerExpression{ + PositiveLiteral: []byte("2"), + Value: big.NewInt(2), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 7, Offset: 7}, + EndPos: ast.Position{Line: 1, Column: 7, Offset: 7}, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 9, Offset: 9}, + }, + }, + result, + ) + }) + + t.Run("constant, negative size", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseType("[Int ; -2 ]") + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: `expected positive integer size for constant sized type`, + Pos: ast.Position{Offset: 7, Line: 1, Column: 7}, + }, + // TODO: improve/avoid error by skipping full negative integer literal + &SyntaxError{ + Message: `expected token ']'`, + Pos: ast.Position{Offset: 8, Line: 1, Column: 8}, + }, + }, + errs, + ) + + require.Nil(t, result) + }) + + t.Run("constant, invalid size", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseType("[Int ; X ]") + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: `expected positive integer size for constant sized type`, + Pos: ast.Position{Offset: 7, Line: 1, Column: 7}, + }, + }, + errs, + ) + + utils.AssertEqualWithDiff(t, + &ast.VariableSizedType{ + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Int", + Pos: ast.Position{Offset: 1, Line: 1, Column: 1}, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 0, Line: 1, Column: 0}, + EndPos: ast.Position{Offset: 9, Line: 1, Column: 9}, + }, + }, + result, + ) + }) + +} + +func TestParseOptionalType(t *testing.T) { + + t.Parallel() + + t.Run("nominal", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseType("Int?") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.OptionalType{ + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Int", + Pos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + EndPos: ast.Position{Line: 1, Column: 3, Offset: 3}, + }, + result, + ) + }) + + t.Run("double", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseType("Int??") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.OptionalType{ + Type: &ast.OptionalType{ + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Int", + Pos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + EndPos: ast.Position{Line: 1, Column: 3, Offset: 3}, + }, + EndPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + }, + result, + ) + }) + + t.Run("triple", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseType("Int???") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.OptionalType{ + Type: &ast.OptionalType{ + Type: &ast.OptionalType{ + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Int", + Pos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + EndPos: ast.Position{Line: 1, Column: 3, Offset: 3}, + }, + EndPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + }, + EndPos: ast.Position{Line: 1, Column: 5, Offset: 5}, + }, + result, + ) + }) +} + +func TestParseReferenceType(t *testing.T) { + + t.Parallel() + + t.Run("unauthorized, nominal", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseType("&Int") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.ReferenceType{ + Authorization: nil, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Int", + Pos: ast.Position{Line: 1, Column: 1, Offset: 1}, + }, + }, + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + result, + ) + }) + + t.Run("authorized, nominal", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseType("auth &Int") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.ReferenceType{ + Authorization: nil, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Int", + Pos: ast.Position{Line: 1, Column: 6, Offset: 6}, + }, + }, + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + result, + ) + }) +} + +func TestParseOptionalReferenceType(t *testing.T) { + + t.Parallel() + + t.Run("unauthorized", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseType("&Int?") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.OptionalType{ + Type: &ast.ReferenceType{ + Authorization: nil, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Int", + Pos: ast.Position{Line: 1, Column: 1, Offset: 1}, + }, + }, + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + EndPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + }, + result, + ) + }) +} + +func TestParseRestrictedType(t *testing.T) { + + t.Parallel() + + t.Run("with restricted type, no restrictions", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseType("T{}") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.IntersectionType{ + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + }, + }, + result, + ) + }) + + t.Run("with restricted type, one restriction", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseType("T{U}") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.IntersectionType{ + Types: []*ast.NominalType{ + { + Identifier: ast.Identifier{ + Identifier: "U", + Pos: ast.Position{Line: 1, Column: 2, Offset: 2}, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 3, Offset: 3}, + }, + }, + result, + ) + }) + + t.Run("with restricted type, two restrictions", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseType("T{U , V }") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.IntersectionType{ + Types: []*ast.NominalType{ + { + Identifier: ast.Identifier{ + Identifier: "U", + Pos: ast.Position{Line: 1, Column: 2, Offset: 2}, + }, + }, + { + Identifier: ast.Identifier{ + Identifier: "V", + Pos: ast.Position{Line: 1, Column: 6, Offset: 6}, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 8, Offset: 8}, + }, + }, + result, + ) + }) + + t.Run("without restricted type, no restrictions", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseType("{}") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.IntersectionType{ + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + }, + }, + result, + ) + }) + + t.Run("without restricted type, one restriction", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseType("{ T }") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.IntersectionType{ + Types: []*ast.NominalType{ + { + Identifier: ast.Identifier{ + Identifier: "T", + Pos: ast.Position{Line: 1, Column: 2, Offset: 2}, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + }, + }, + result, + ) + }) + + t.Run("invalid: without restricted type, missing type after comma", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseType("{ T , }") + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "missing type after comma", + Pos: ast.Position{Offset: 6, Line: 1, Column: 6}, + }, + }, + errs, + ) + + utils.AssertEqualWithDiff(t, + &ast.IntersectionType{ + Types: []*ast.NominalType{ + { + Identifier: ast.Identifier{ + Identifier: "T", + Pos: ast.Position{Line: 1, Column: 2, Offset: 2}, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 6, Offset: 6}, + }, + }, + result, + ) + }) + + t.Run("invalid: without restricted type, type without comma", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseType("{ T U }") + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "unexpected type", + Pos: ast.Position{Offset: 4, Line: 1, Column: 4}, + }, + }, + errs, + ) + + // TODO: return type + assert.Nil(t, result) + }) + + t.Run("invalid: without restricted type, colon", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseType("{ T , U : V }") + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "unexpected colon in restricted type", + Pos: ast.Position{Offset: 8, Line: 1, Column: 8}, + }, + }, + errs, + ) + + // TODO: return type + assert.Nil(t, result) + }) + + t.Run("invalid: with restricted type, colon", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseType("T{U , V : W }") + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: `unexpected token: got ':', expected ',' or '}'`, + Pos: ast.Position{Offset: 8, Line: 1, Column: 8}, + }, + }, + errs, + ) + + // TODO: return type + assert.Nil(t, result) + }) + + t.Run("invalid: without restricted type, first is non-nominal", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseType("{[T]}") + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "non-nominal type in restriction list: [T]", + Pos: ast.Position{Offset: 5, Line: 1, Column: 5}, + }, + }, + errs, + ) + + // TODO: return type with non-nominal restrictions + assert.Nil(t, result) + }) + + t.Run("invalid: with restricted type, first is non-nominal", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseType("T{[U]}") + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "unexpected non-nominal type: [U]", + Pos: ast.Position{Offset: 5, Line: 1, Column: 5}, + }, + }, + errs, + ) + + // TODO: return type + assert.Nil(t, result) + }) + + t.Run("invalid: without restricted type, second is non-nominal", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseType("{T, [U]}") + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "non-nominal type in restriction list: [U]", + Pos: ast.Position{Offset: 7, Line: 1, Column: 7}, + }, + }, + errs, + ) + + // TODO: return type + assert.Nil(t, result) + }) + + t.Run("invalid: with restricted type, second is non-nominal", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseType("T{U, [V]}") + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "unexpected non-nominal type: [V]", + Pos: ast.Position{Offset: 8, Line: 1, Column: 8}, + }, + }, + errs, + ) + + // TODO: return type + assert.Nil(t, result) + }) + + t.Run("invalid: without restricted type, missing end", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseType("{") + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "invalid end of input, expected type", + Pos: ast.Position{Offset: 1, Line: 1, Column: 1}, + }, + }, + errs, + ) + + assert.Nil(t, result) + }) + + t.Run("invalid: with restricted type, missing end", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseType("T{") + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "invalid end of input, expected type", + Pos: ast.Position{Offset: 2, Line: 1, Column: 2}, + }, + }, + errs, + ) + + assert.Nil(t, result) + }) + + t.Run("invalid: without restricted type, missing end after type", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseType("{U") + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "invalid end of input, expected '}'", + Pos: ast.Position{Offset: 2, Line: 1, Column: 2}, + }, + }, + errs, + ) + + assert.Nil(t, result) + }) + + t.Run("invalid: with restricted type, missing end after type", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseType("T{U") + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "invalid end of input, expected '}'", + Pos: ast.Position{Offset: 3, Line: 1, Column: 3}, + }, + }, + errs, + ) + + assert.Nil(t, result) + }) + + t.Run("invalid: without restricted type, missing end after comma", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseType("{U,") + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "invalid end of input, expected type", + Pos: ast.Position{Offset: 3, Line: 1, Column: 3}, + }, + }, + errs, + ) + + assert.Nil(t, result) + }) + + t.Run("invalid: with restricted type, missing end after comma", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseType("T{U,") + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "invalid end of input, expected type", + Pos: ast.Position{Offset: 4, Line: 1, Column: 4}, + }, + }, + errs, + ) + + assert.Nil(t, result) + }) + + t.Run("invalid: without restricted type, just comma", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseType("{,}") + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "unexpected comma in restricted type", + Pos: ast.Position{Offset: 1, Line: 1, Column: 1}, + }, + }, + errs, + ) + + assert.Nil(t, result) + }) + + t.Run("invalid: with restricted type, just comma", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseType("T{,}") + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "unexpected comma", + Pos: ast.Position{Offset: 2, Line: 1, Column: 2}, + }, + }, + errs, + ) + + assert.Nil(t, result) + }) +} + +func TestParseDictionaryType(t *testing.T) { + + t.Parallel() + + t.Run("valid", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseType("{T: U}") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.DictionaryType{ + KeyType: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "T", + Pos: ast.Position{Line: 1, Column: 1, Offset: 1}, + }, + }, + ValueType: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "U", + Pos: ast.Position{Line: 1, Column: 4, Offset: 4}, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 5, Offset: 5}, + }, + }, + result, + ) + }) + + t.Run("invalid, missing value type", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseType("{T:}") + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "missing dictionary value type", + Pos: ast.Position{Offset: 3, Line: 1, Column: 3}, + }, + }, + errs, + ) + + utils.AssertEqualWithDiff(t, + &ast.DictionaryType{ + KeyType: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "T", + Pos: ast.Position{Line: 1, Column: 1, Offset: 1}, + }, + }, + ValueType: nil, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 3, Offset: 3}, + }, + }, + result, + ) + }) + + t.Run("invalid, missing key and value type", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseType("{:}") + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "unexpected colon in dictionary type", + Pos: ast.Position{Offset: 1, Line: 1, Column: 1}, + }, + }, + errs, + ) + + assert.Nil(t, result) + }) + + t.Run("invalid, missing key type", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseType("{:U}") + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "unexpected colon in dictionary type", + Pos: ast.Position{Offset: 1, Line: 1, Column: 1}, + }, + }, + errs, + ) + + // TODO: return type + assert.Nil(t, result) + }) + + t.Run("invalid, unexpected comma after value type", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseType("{T:U,}") + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "unexpected comma in dictionary type", + Pos: ast.Position{Offset: 4, Line: 1, Column: 4}, + }, + }, + errs, + ) + + // TODO: return type + assert.Nil(t, result) + }) + + t.Run("invalid, unexpected colon after value type", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseType("{T:U:}") + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "unexpected colon in dictionary type", + Pos: ast.Position{Offset: 4, Line: 1, Column: 4}, + }, + }, + errs, + ) + + // TODO: return type + assert.Nil(t, result) + }) + + t.Run("invalid, unexpected colon after colon", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseType("{T::U}") + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "unexpected colon in dictionary type", + Pos: ast.Position{Offset: 3, Line: 1, Column: 3}, + }, + }, + errs, + ) + + // TODO: return type + assert.Nil(t, result) + }) + + t.Run("invalid, missing value type after colon", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseType("{T:") + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "invalid end of input, expected type", + Pos: ast.Position{Offset: 3, Line: 1, Column: 3}, + }, + }, + errs, + ) + + assert.Nil(t, result) + }) + + t.Run("invalid, missing end after key type and value type", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseType("{T:U") + utils.AssertEqualWithDiff(t, + []error{ + &SyntaxError{ + Message: "invalid end of input, expected '}'", + Pos: ast.Position{Offset: 4, Line: 1, Column: 4}, + }, + }, + errs, + ) + + assert.Nil(t, result) + }) +} + +func TestParseFunctionType(t *testing.T) { + + t.Parallel() + + t.Run("no parameters, Void return type", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseType("(():Void)") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.FunctionType{ + ParameterTypeAnnotations: nil, + ReturnTypeAnnotation: &ast.TypeAnnotation{ + IsResource: false, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Void", + Pos: ast.Position{Line: 1, Column: 4, Offset: 4}, + }, + }, + StartPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + }, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 8, Offset: 8}, + }, + }, + result, + ) + }) + + t.Run("three parameters, Int return type", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseType("( ( String , Bool , @R ) : Int)") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.FunctionType{ + ParameterTypeAnnotations: []*ast.TypeAnnotation{ + { + IsResource: false, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "String", + Pos: ast.Position{Line: 1, Column: 4, Offset: 4}, + }, + }, + StartPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + }, + { + IsResource: false, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Bool", + Pos: ast.Position{Line: 1, Column: 13, Offset: 13}, + }, + }, + StartPos: ast.Position{Line: 1, Column: 13, Offset: 13}, + }, + { + IsResource: true, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "R", + Pos: ast.Position{Line: 1, Column: 21, Offset: 21}, + }, + }, + StartPos: ast.Position{Line: 1, Column: 20, Offset: 20}, + }, + }, + ReturnTypeAnnotation: &ast.TypeAnnotation{ + IsResource: false, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Int", + Pos: ast.Position{Line: 1, Column: 27, Offset: 27}, + }, + }, + StartPos: ast.Position{Line: 1, Column: 27, Offset: 27}, + }, + Range: ast.Range{ + StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, + EndPos: ast.Position{Line: 1, Column: 30, Offset: 30}, + }, + }, + result, + ) + }) +} + +func TestParseInstantiationType(t *testing.T) { + + t.Parallel() + + t.Run("no type arguments", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseType("T<>") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.InstantiationType{ + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "T", + Pos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + TypeArgumentsStartPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + EndPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + }, + result, + ) + }) + + t.Run("one type argument, no spaces", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseType("T") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.InstantiationType{ + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "T", + Pos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + TypeArguments: []*ast.TypeAnnotation{ + { + IsResource: false, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "U", + Pos: ast.Position{Line: 1, Column: 2, Offset: 2}, + }, + }, + StartPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + }, + }, + TypeArgumentsStartPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + EndPos: ast.Position{Line: 1, Column: 3, Offset: 3}, + }, + result, + ) + }) + + t.Run("one type argument, with spaces", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseType("T< U >") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.InstantiationType{ + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "T", + Pos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + TypeArguments: []*ast.TypeAnnotation{ + { + IsResource: false, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "U", + Pos: ast.Position{Line: 1, Column: 3, Offset: 3}, + }, + }, + StartPos: ast.Position{Line: 1, Column: 3, Offset: 3}, + }, + }, + TypeArgumentsStartPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + EndPos: ast.Position{Line: 1, Column: 5, Offset: 5}, + }, + result, + ) + }) + + t.Run("two type arguments, with spaces", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseType("T< U , @V >") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.InstantiationType{ + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "T", + Pos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + TypeArguments: []*ast.TypeAnnotation{ + { + IsResource: false, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "U", + Pos: ast.Position{Line: 1, Column: 3, Offset: 3}, + }, + }, + StartPos: ast.Position{Line: 1, Column: 3, Offset: 3}, + }, + { + IsResource: true, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "V", + Pos: ast.Position{Line: 1, Column: 8, Offset: 8}, + }, + }, + StartPos: ast.Position{Line: 1, Column: 7, Offset: 7}, + }, + }, + TypeArgumentsStartPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + EndPos: ast.Position{Line: 1, Column: 10, Offset: 10}, + }, + result, + ) + }) + + t.Run("one type argument, no spaces", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseType("T") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.InstantiationType{ + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "T", + Pos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + TypeArguments: []*ast.TypeAnnotation{ + { + IsResource: false, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "U", + Pos: ast.Position{Line: 1, Column: 2, Offset: 2}, + }, + }, + StartPos: ast.Position{Line: 1, Column: 2, Offset: 2}, + }, + }, + TypeArgumentsStartPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + EndPos: ast.Position{Line: 1, Column: 3, Offset: 3}, + }, + result, + ) + }) + + t.Run("one type argument, nested, with spaces", func(t *testing.T) { + + t.Parallel() + + result, errs := testParseType("T< U< V > >") + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + &ast.InstantiationType{ + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "T", + Pos: ast.Position{Line: 1, Column: 0, Offset: 0}, + }, + }, + TypeArguments: []*ast.TypeAnnotation{ + { + IsResource: false, + Type: &ast.InstantiationType{ + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "U", + Pos: ast.Position{Line: 1, Column: 3, Offset: 3}, + }, + }, + TypeArguments: []*ast.TypeAnnotation{ + { + IsResource: false, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "V", + Pos: ast.Position{Line: 1, Column: 6, Offset: 6}, + }, + }, + StartPos: ast.Position{Line: 1, Column: 6, Offset: 6}, + }, + }, + TypeArgumentsStartPos: ast.Position{Line: 1, Column: 4, Offset: 4}, + EndPos: ast.Position{Line: 1, Column: 8, Offset: 8}, + }, + StartPos: ast.Position{Line: 1, Column: 3, Offset: 3}, + }, + }, + TypeArgumentsStartPos: ast.Position{Line: 1, Column: 1, Offset: 1}, + EndPos: ast.Position{Line: 1, Column: 11, Offset: 11}, + }, + result, + ) + }) +} + +func TestParseParametersAndArrayTypes(t *testing.T) { + + t.Parallel() + + const code = ` + pub fun test(a: Int32, b: [Int32; 2], c: [[Int32; 3]]): [[Int64]] {} + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.FunctionDeclaration{ + Access: ast.AccessAll, + Identifier: ast.Identifier{ + Identifier: "test", + Pos: ast.Position{Offset: 11, Line: 2, Column: 10}, + }, + ParameterList: &ast.ParameterList{ + Parameters: []*ast.Parameter{ + { + Identifier: ast.Identifier{ + Identifier: "a", + Pos: ast.Position{Offset: 16, Line: 2, Column: 15}, + }, + TypeAnnotation: &ast.TypeAnnotation{ + IsResource: false, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Int32", + Pos: ast.Position{Offset: 19, Line: 2, Column: 18}, + }, + }, + StartPos: ast.Position{Offset: 19, Line: 2, Column: 18}, + }, + StartPos: ast.Position{Offset: 16, Line: 2, Column: 15}, + }, + { + Identifier: ast.Identifier{ + Identifier: "b", + Pos: ast.Position{Offset: 26, Line: 2, Column: 25}, + }, + TypeAnnotation: &ast.TypeAnnotation{ + IsResource: false, + Type: &ast.ConstantSizedType{ + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Int32", + Pos: ast.Position{Offset: 30, Line: 2, Column: 29}, + }, + }, + Size: &ast.IntegerExpression{ + PositiveLiteral: []byte("2"), + Value: big.NewInt(2), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 37, Line: 2, Column: 36}, + EndPos: ast.Position{Offset: 37, Line: 2, Column: 36}, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 29, Line: 2, Column: 28}, + EndPos: ast.Position{Offset: 38, Line: 2, Column: 37}, + }, + }, + StartPos: ast.Position{Offset: 29, Line: 2, Column: 28}, + }, + StartPos: ast.Position{Offset: 26, Line: 2, Column: 25}, + }, + { + Identifier: ast.Identifier{ + Identifier: "c", + Pos: ast.Position{Offset: 41, Line: 2, Column: 40}, + }, + TypeAnnotation: &ast.TypeAnnotation{ + IsResource: false, + Type: &ast.VariableSizedType{ + Type: &ast.ConstantSizedType{ + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Int32", + Pos: ast.Position{Offset: 46, Line: 2, Column: 45}, + }, + }, + Size: &ast.IntegerExpression{ + PositiveLiteral: []byte("3"), + Value: big.NewInt(3), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 53, Line: 2, Column: 52}, + EndPos: ast.Position{Offset: 53, Line: 2, Column: 52}, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 45, Line: 2, Column: 44}, + EndPos: ast.Position{Offset: 54, Line: 2, Column: 53}, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 44, Line: 2, Column: 43}, + EndPos: ast.Position{Offset: 55, Line: 2, Column: 54}, + }, + }, + StartPos: ast.Position{Offset: 44, Line: 2, Column: 43}, + }, + StartPos: ast.Position{Offset: 41, Line: 2, Column: 40}, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 15, Line: 2, Column: 14}, + EndPos: ast.Position{Offset: 56, Line: 2, Column: 55}, + }, + }, + ReturnTypeAnnotation: &ast.TypeAnnotation{ + IsResource: false, + Type: &ast.VariableSizedType{ + Type: &ast.VariableSizedType{ + Type: &ast.NominalType{ + Identifier: ast.Identifier{Identifier: "Int64", + Pos: ast.Position{Offset: 61, Line: 2, Column: 60}, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 60, Line: 2, Column: 59}, + EndPos: ast.Position{Offset: 66, Line: 2, Column: 65}, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 59, Line: 2, Column: 58}, + EndPos: ast.Position{Offset: 67, Line: 2, Column: 66}, + }, + }, + StartPos: ast.Position{Offset: 59, Line: 2, Column: 58}, + }, + FunctionBlock: &ast.FunctionBlock{ + Block: &ast.Block{ + Range: ast.Range{ + StartPos: ast.Position{Offset: 69, Line: 2, Column: 68}, + EndPos: ast.Position{Offset: 70, Line: 2, Column: 69}, + }, + }, + }, + StartPos: ast.Position{Offset: 3, Line: 2, Column: 2}, + }, + }, + result.Declarations(), + ) +} + +func TestParseDictionaryTypeInVariableDeclaration(t *testing.T) { + + t.Parallel() + + const code = ` + let x: {String: Int} = {} + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.VariableDeclaration{ + Access: ast.AccessNotSpecified, + IsConstant: true, + Identifier: ast.Identifier{Identifier: "x", + Pos: ast.Position{Offset: 10, Line: 2, Column: 9}, + }, + TypeAnnotation: &ast.TypeAnnotation{ + IsResource: false, + Type: &ast.DictionaryType{ + KeyType: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "String", + Pos: ast.Position{Offset: 14, Line: 2, Column: 13}, + }, + }, + ValueType: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Int", + Pos: ast.Position{Offset: 22, Line: 2, Column: 21}, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 13, Line: 2, Column: 12}, + EndPos: ast.Position{Offset: 25, Line: 2, Column: 24}, + }, + }, + StartPos: ast.Position{Offset: 13, Line: 2, Column: 12}, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationCopy, + Pos: ast.Position{Offset: 27, Line: 2, Column: 26}, + }, + Value: &ast.DictionaryExpression{ + Range: ast.Range{ + StartPos: ast.Position{Offset: 29, Line: 2, Column: 28}, + EndPos: ast.Position{Offset: 30, Line: 2, Column: 29}, + }, + }, + StartPos: ast.Position{Offset: 6, Line: 2, Column: 5}, + }, + }, + result.Declarations(), + ) +} + +func TestParseIntegerTypes(t *testing.T) { + + t.Parallel() + + const code = ` + let a: Int8 = 1 + let b: Int16 = 2 + let c: Int32 = 3 + let d: Int64 = 4 + let e: UInt8 = 5 + let f: UInt16 = 6 + let g: UInt32 = 7 + let h: UInt64 = 8 + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + a := &ast.VariableDeclaration{ + Access: ast.AccessNotSpecified, + Identifier: ast.Identifier{ + Identifier: "a", + Pos: ast.Position{Offset: 7, Line: 2, Column: 6}, + }, + + IsConstant: true, + TypeAnnotation: &ast.TypeAnnotation{ + IsResource: false, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Int8", + Pos: ast.Position{Offset: 10, Line: 2, Column: 9}, + }, + }, + StartPos: ast.Position{Offset: 10, Line: 2, Column: 9}, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationCopy, + Pos: ast.Position{Offset: 15, Line: 2, Column: 14}, + }, + Value: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 17, Line: 2, Column: 16}, + EndPos: ast.Position{Offset: 17, Line: 2, Column: 16}, + }, + }, + StartPos: ast.Position{Offset: 3, Line: 2, Column: 2}, + } + b := &ast.VariableDeclaration{ + Access: ast.AccessNotSpecified, + Identifier: ast.Identifier{ + Identifier: "b", + Pos: ast.Position{Offset: 25, Line: 3, Column: 6}, + }, + IsConstant: true, + TypeAnnotation: &ast.TypeAnnotation{ + IsResource: false, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Int16", + Pos: ast.Position{Offset: 28, Line: 3, Column: 9}, + }, + }, + StartPos: ast.Position{Offset: 28, Line: 3, Column: 9}, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationCopy, + Pos: ast.Position{Offset: 34, Line: 3, Column: 15}, + }, + Value: &ast.IntegerExpression{ + PositiveLiteral: []byte("2"), + Value: big.NewInt(2), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 36, Line: 3, Column: 17}, + EndPos: ast.Position{Offset: 36, Line: 3, Column: 17}, + }, + }, + StartPos: ast.Position{Offset: 21, Line: 3, Column: 2}, + } + c := &ast.VariableDeclaration{ + Access: ast.AccessNotSpecified, + Identifier: ast.Identifier{ + Identifier: "c", + Pos: ast.Position{Offset: 44, Line: 4, Column: 6}, + }, + IsConstant: true, + TypeAnnotation: &ast.TypeAnnotation{ + IsResource: false, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Int32", + Pos: ast.Position{Offset: 47, Line: 4, Column: 9}, + }, + }, + StartPos: ast.Position{Offset: 47, Line: 4, Column: 9}, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationCopy, + Pos: ast.Position{Offset: 53, Line: 4, Column: 15}, + }, + Value: &ast.IntegerExpression{ + PositiveLiteral: []byte("3"), + Value: big.NewInt(3), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 55, Line: 4, Column: 17}, + EndPos: ast.Position{Offset: 55, Line: 4, Column: 17}, + }, + }, + StartPos: ast.Position{Offset: 40, Line: 4, Column: 2}, + } + d := &ast.VariableDeclaration{ + Access: ast.AccessNotSpecified, + Identifier: ast.Identifier{ + Identifier: "d", + Pos: ast.Position{Offset: 63, Line: 5, Column: 6}, + }, + IsConstant: true, + TypeAnnotation: &ast.TypeAnnotation{ + IsResource: false, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Int64", + Pos: ast.Position{Offset: 66, Line: 5, Column: 9}, + }, + }, + StartPos: ast.Position{Offset: 66, Line: 5, Column: 9}, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationCopy, + Pos: ast.Position{Offset: 72, Line: 5, Column: 15}, + }, + Value: &ast.IntegerExpression{ + PositiveLiteral: []byte("4"), + Value: big.NewInt(4), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 74, Line: 5, Column: 17}, + EndPos: ast.Position{Offset: 74, Line: 5, Column: 17}, + }, + }, + StartPos: ast.Position{Offset: 59, Line: 5, Column: 2}, + } + e := &ast.VariableDeclaration{ + Access: ast.AccessNotSpecified, + Identifier: ast.Identifier{ + Identifier: "e", + Pos: ast.Position{Offset: 82, Line: 6, Column: 6}, + }, + IsConstant: true, + TypeAnnotation: &ast.TypeAnnotation{ + IsResource: false, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "UInt8", + Pos: ast.Position{Offset: 85, Line: 6, Column: 9}, + }, + }, + StartPos: ast.Position{Offset: 85, Line: 6, Column: 9}, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationCopy, + Pos: ast.Position{Offset: 91, Line: 6, Column: 15}, + }, + Value: &ast.IntegerExpression{ + PositiveLiteral: []byte("5"), + Value: big.NewInt(5), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 93, Line: 6, Column: 17}, + EndPos: ast.Position{Offset: 93, Line: 6, Column: 17}, + }, + }, + StartPos: ast.Position{Offset: 78, Line: 6, Column: 2}, + } + f := &ast.VariableDeclaration{ + Access: ast.AccessNotSpecified, + Identifier: ast.Identifier{ + Identifier: "f", + Pos: ast.Position{Offset: 101, Line: 7, Column: 6}, + }, + IsConstant: true, + TypeAnnotation: &ast.TypeAnnotation{ + IsResource: false, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "UInt16", + Pos: ast.Position{Offset: 104, Line: 7, Column: 9}, + }, + }, + StartPos: ast.Position{Offset: 104, Line: 7, Column: 9}, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationCopy, + Pos: ast.Position{Offset: 111, Line: 7, Column: 16}, + }, + Value: &ast.IntegerExpression{ + PositiveLiteral: []byte("6"), + Value: big.NewInt(6), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 113, Line: 7, Column: 18}, + EndPos: ast.Position{Offset: 113, Line: 7, Column: 18}, + }, + }, + StartPos: ast.Position{Offset: 97, Line: 7, Column: 2}, + } + g := &ast.VariableDeclaration{ + Access: ast.AccessNotSpecified, + Identifier: ast.Identifier{ + Identifier: "g", + Pos: ast.Position{Offset: 121, Line: 8, Column: 6}, + }, + IsConstant: true, + TypeAnnotation: &ast.TypeAnnotation{ + IsResource: false, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "UInt32", + Pos: ast.Position{Offset: 124, Line: 8, Column: 9}, + }, + }, + StartPos: ast.Position{Offset: 124, Line: 8, Column: 9}, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationCopy, + Pos: ast.Position{Offset: 131, Line: 8, Column: 16}, + }, + Value: &ast.IntegerExpression{ + PositiveLiteral: []byte("7"), + Value: big.NewInt(7), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 133, Line: 8, Column: 18}, + EndPos: ast.Position{Offset: 133, Line: 8, Column: 18}, + }, + }, + StartPos: ast.Position{Offset: 117, Line: 8, Column: 2}, + } + h := &ast.VariableDeclaration{ + Access: ast.AccessNotSpecified, + Identifier: ast.Identifier{ + Identifier: "h", + Pos: ast.Position{Offset: 141, Line: 9, Column: 6}, + }, + IsConstant: true, + TypeAnnotation: &ast.TypeAnnotation{ + IsResource: false, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "UInt64", + Pos: ast.Position{Offset: 144, Line: 9, Column: 9}, + }, + }, + StartPos: ast.Position{Offset: 144, Line: 9, Column: 9}, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationCopy, + Pos: ast.Position{Offset: 151, Line: 9, Column: 16}, + }, + Value: &ast.IntegerExpression{ + PositiveLiteral: []byte("8"), + Value: big.NewInt(8), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 153, Line: 9, Column: 18}, + EndPos: ast.Position{Offset: 153, Line: 9, Column: 18}, + }, + }, + StartPos: ast.Position{Offset: 137, Line: 9, Column: 2}, + } + + utils.AssertEqualWithDiff(t, + []ast.Declaration{a, b, c, d, e, f, g, h}, + result.Declarations(), + ) +} + +func TestParseFunctionTypeInVariableDeclaration(t *testing.T) { + + t.Parallel() + + const code = ` + let add: ((Int8, Int16): Int32) = nothing + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.VariableDeclaration{ + Access: ast.AccessNotSpecified, + Identifier: ast.Identifier{ + Identifier: "add", + Pos: ast.Position{Offset: 7, Line: 2, Column: 6}, + }, + IsConstant: true, + TypeAnnotation: &ast.TypeAnnotation{ + IsResource: false, + Type: &ast.FunctionType{ + ParameterTypeAnnotations: []*ast.TypeAnnotation{ + { + IsResource: false, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Int8", + Pos: ast.Position{Offset: 14, Line: 2, Column: 13}, + }, + }, + StartPos: ast.Position{Offset: 14, Line: 2, Column: 13}, + }, + { + IsResource: false, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Int16", + Pos: ast.Position{Offset: 20, Line: 2, Column: 19}, + }, + }, + StartPos: ast.Position{Offset: 20, Line: 2, Column: 19}, + }, + }, + ReturnTypeAnnotation: &ast.TypeAnnotation{ + IsResource: false, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Int32", + Pos: ast.Position{Offset: 28, Line: 2, Column: 27}, + }, + }, + StartPos: ast.Position{Offset: 28, Line: 2, Column: 27}, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 12, Line: 2, Column: 11}, + EndPos: ast.Position{Offset: 33, Line: 2, Column: 32}, + }, + }, + StartPos: ast.Position{Offset: 12, Line: 2, Column: 11}, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationCopy, + Pos: ast.Position{Offset: 35, Line: 2, Column: 34}, + }, + Value: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "nothing", + Pos: ast.Position{Offset: 37, Line: 2, Column: 36}, + }, + }, + StartPos: ast.Position{Offset: 3, Line: 2, Column: 2}, + }, + }, + result.Declarations(), + ) +} + +func TestParseFunctionArrayType(t *testing.T) { + + t.Parallel() + + const code = ` + let test: [((Int8): Int16); 2] = [] + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.VariableDeclaration{ + Access: ast.AccessNotSpecified, + Identifier: ast.Identifier{ + Identifier: "test", + Pos: ast.Position{Offset: 7, Line: 2, Column: 6}, + }, + + IsConstant: true, + TypeAnnotation: &ast.TypeAnnotation{ + IsResource: false, + Type: &ast.ConstantSizedType{ + Type: &ast.FunctionType{ + ParameterTypeAnnotations: []*ast.TypeAnnotation{ + { + IsResource: false, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Int8", + Pos: ast.Position{Offset: 16, Line: 2, Column: 15}, + }, + }, + StartPos: ast.Position{Offset: 16, Line: 2, Column: 15}, + }, + }, + ReturnTypeAnnotation: &ast.TypeAnnotation{ + IsResource: false, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Int16", + Pos: ast.Position{Offset: 23, Line: 2, Column: 22}, + }, + }, + StartPos: ast.Position{Offset: 23, Line: 2, Column: 22}, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 14, Line: 2, Column: 13}, + EndPos: ast.Position{Offset: 28, Line: 2, Column: 27}, + }, + }, + Size: &ast.IntegerExpression{ + PositiveLiteral: []byte("2"), + Value: big.NewInt(2), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 31, Line: 2, Column: 30}, + EndPos: ast.Position{Offset: 31, Line: 2, Column: 30}, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 13, Line: 2, Column: 12}, + EndPos: ast.Position{Offset: 32, Line: 2, Column: 31}, + }, + }, + StartPos: ast.Position{Offset: 13, Line: 2, Column: 12}, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationCopy, + Pos: ast.Position{Offset: 34, Line: 2, Column: 33}, + }, + Value: &ast.ArrayExpression{ + Range: ast.Range{ + StartPos: ast.Position{Offset: 36, Line: 2, Column: 35}, + EndPos: ast.Position{Offset: 37, Line: 2, Column: 36}, + }, + }, + StartPos: ast.Position{Offset: 3, Line: 2, Column: 2}, + }, + }, + result.Declarations(), + ) +} + +func TestParseFunctionTypeWithArrayReturnType(t *testing.T) { + + t.Parallel() + + const code = ` + let test: ((Int8): [Int16; 2]) = nothing + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.VariableDeclaration{ + Access: ast.AccessNotSpecified, + Identifier: ast.Identifier{ + Identifier: "test", + Pos: ast.Position{Offset: 7, Line: 2, Column: 6}, + }, + IsConstant: true, + TypeAnnotation: &ast.TypeAnnotation{ + IsResource: false, + Type: &ast.FunctionType{ + ParameterTypeAnnotations: []*ast.TypeAnnotation{ + { + IsResource: false, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Int8", + Pos: ast.Position{Offset: 15, Line: 2, Column: 14}, + }, + }, + StartPos: ast.Position{Offset: 15, Line: 2, Column: 14}, + }, + }, + ReturnTypeAnnotation: &ast.TypeAnnotation{ + IsResource: false, + Type: &ast.ConstantSizedType{ + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Int16", + Pos: ast.Position{Offset: 23, Line: 2, Column: 22}, + }, + }, + Size: &ast.IntegerExpression{ + PositiveLiteral: []byte("2"), + Value: big.NewInt(2), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 30, Line: 2, Column: 29}, + EndPos: ast.Position{Offset: 30, Line: 2, Column: 29}, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 22, Line: 2, Column: 21}, + EndPos: ast.Position{Offset: 31, Line: 2, Column: 30}, + }, + }, + StartPos: ast.Position{Offset: 22, Line: 2, Column: 21}, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 13, Line: 2, Column: 12}, + EndPos: ast.Position{Offset: 32, Line: 2, Column: 31}, + }, + }, + StartPos: ast.Position{Offset: 13, Line: 2, Column: 12}, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationCopy, + Pos: ast.Position{Offset: 34, Line: 2, Column: 33}, + }, + Value: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "nothing", + Pos: ast.Position{Offset: 36, Line: 2, Column: 35}, + }, + }, + StartPos: ast.Position{Offset: 3, Line: 2, Column: 2}, + }, + }, + result.Declarations(), + ) +} + +func TestParseFunctionTypeWithFunctionReturnTypeInParentheses(t *testing.T) { + + t.Parallel() + + const code = ` + let test: ((Int8): ((Int16): Int32)) = nothing + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.VariableDeclaration{ + Access: ast.AccessNotSpecified, + Identifier: ast.Identifier{ + Identifier: "test", + Pos: ast.Position{Offset: 7, Line: 2, Column: 6}, + }, + IsConstant: true, + TypeAnnotation: &ast.TypeAnnotation{ + IsResource: false, + Type: &ast.FunctionType{ + ParameterTypeAnnotations: []*ast.TypeAnnotation{ + { + IsResource: false, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Int8", + Pos: ast.Position{Offset: 15, Line: 2, Column: 14}, + }, + }, + StartPos: ast.Position{Offset: 15, Line: 2, Column: 14}, + }, + }, + ReturnTypeAnnotation: &ast.TypeAnnotation{ + IsResource: false, + Type: &ast.FunctionType{ + ParameterTypeAnnotations: []*ast.TypeAnnotation{ + { + IsResource: false, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Int16", + Pos: ast.Position{Offset: 24, Line: 2, Column: 23}, + }, + }, + StartPos: ast.Position{Offset: 24, Line: 2, Column: 23}, + }, + }, + ReturnTypeAnnotation: &ast.TypeAnnotation{ + IsResource: false, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Int32", + Pos: ast.Position{Offset: 32, Line: 2, Column: 31}, + }, + }, + StartPos: ast.Position{Offset: 32, Line: 2, Column: 31}, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 22, Line: 2, Column: 21}, + EndPos: ast.Position{Offset: 37, Line: 2, Column: 36}, + }, + }, + StartPos: ast.Position{Offset: 22, Line: 2, Column: 21}, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 13, Line: 2, Column: 12}, + EndPos: ast.Position{Offset: 38, Line: 2, Column: 37}, + }, + }, + StartPos: ast.Position{Offset: 13, Line: 2, Column: 12}, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationCopy, + Pos: ast.Position{Offset: 40, Line: 2, Column: 39}, + }, + Value: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "nothing", + Pos: ast.Position{Offset: 42, Line: 2, Column: 41}, + }, + }, + StartPos: ast.Position{Offset: 3, Line: 2, Column: 2}, + }, + }, + result.Declarations(), + ) +} + +func TestParseFunctionTypeWithFunctionReturnType(t *testing.T) { + + t.Parallel() + + const code = ` + let test: ((Int8): ((Int16): Int32)) = nothing + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.VariableDeclaration{ + Access: ast.AccessNotSpecified, + Identifier: ast.Identifier{ + Identifier: "test", + Pos: ast.Position{Offset: 7, Line: 2, Column: 6}, + }, + IsConstant: true, + TypeAnnotation: &ast.TypeAnnotation{ + IsResource: false, + Type: &ast.FunctionType{ + ParameterTypeAnnotations: []*ast.TypeAnnotation{ + { + IsResource: false, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Int8", + Pos: ast.Position{Offset: 15, Line: 2, Column: 14}, + }, + }, + StartPos: ast.Position{Offset: 15, Line: 2, Column: 14}, + }, + }, + ReturnTypeAnnotation: &ast.TypeAnnotation{ + IsResource: false, + Type: &ast.FunctionType{ + ParameterTypeAnnotations: []*ast.TypeAnnotation{ + { + IsResource: false, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Int16", + Pos: ast.Position{Offset: 24, Line: 2, Column: 23}, + }, + }, + StartPos: ast.Position{Offset: 24, Line: 2, Column: 23}, + }, + }, + ReturnTypeAnnotation: &ast.TypeAnnotation{ + IsResource: false, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Int32", + Pos: ast.Position{Offset: 32, Line: 2, Column: 31}, + }, + }, + StartPos: ast.Position{Offset: 32, Line: 2, Column: 31}, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 22, Line: 2, Column: 21}, + EndPos: ast.Position{Offset: 37, Line: 2, Column: 36}, + }, + }, + StartPos: ast.Position{Offset: 22, Line: 2, Column: 21}, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 13, Line: 2, Column: 12}, + EndPos: ast.Position{Offset: 38, Line: 2, Column: 37}, + }, + }, + StartPos: ast.Position{Offset: 13, Line: 2, Column: 12}, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationCopy, + Pos: ast.Position{Offset: 40, Line: 2, Column: 39}, + }, + Value: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "nothing", + Pos: ast.Position{Offset: 42, Line: 2, Column: 41}, + }, + }, + StartPos: ast.Position{Offset: 3, Line: 2, Column: 2}, + }, + }, + result.Declarations(), + ) +} + +func TestParseOptionalTypeDouble(t *testing.T) { + + t.Parallel() + + const code = ` + let x: Int?? = 1 + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.VariableDeclaration{ + Access: ast.AccessNotSpecified, + IsConstant: true, + Identifier: ast.Identifier{ + Identifier: "x", + Pos: ast.Position{Offset: 12, Line: 2, Column: 11}, + }, + TypeAnnotation: &ast.TypeAnnotation{ + IsResource: false, + Type: &ast.OptionalType{ + Type: &ast.OptionalType{ + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Int", + Pos: ast.Position{Offset: 15, Line: 2, Column: 14}, + }, + }, + EndPos: ast.Position{Offset: 18, Line: 2, Column: 17}, + }, + EndPos: ast.Position{Offset: 19, Line: 2, Column: 18}, + }, + StartPos: ast.Position{Offset: 15, Line: 2, Column: 14}, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationCopy, + Pos: ast.Position{Offset: 21, Line: 2, Column: 20}, + }, + Value: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 23, Line: 2, Column: 22}, + EndPos: ast.Position{Offset: 23, Line: 2, Column: 22}, + }, + }, + StartPos: ast.Position{Offset: 8, Line: 2, Column: 7}, + }, + }, + result.Declarations(), + ) +} + +func TestParseFunctionTypeWithResourceTypeAnnotation(t *testing.T) { + + t.Parallel() + + const code = ` + let f: ((): @R) = g + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.VariableDeclaration{ + Access: ast.AccessNotSpecified, + IsConstant: true, + Identifier: ast.Identifier{ + Identifier: "f", + Pos: ast.Position{Offset: 13, Line: 2, Column: 12}, + }, + TypeAnnotation: &ast.TypeAnnotation{ + IsResource: false, + Type: &ast.FunctionType{ + ParameterTypeAnnotations: nil, + ReturnTypeAnnotation: &ast.TypeAnnotation{ + IsResource: true, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "R", + Pos: ast.Position{Offset: 22, Line: 2, Column: 21}, + }, + }, + StartPos: ast.Position{Offset: 21, Line: 2, Column: 20}, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 16, Line: 2, Column: 15}, + EndPos: ast.Position{Offset: 23, Line: 2, Column: 22}, + }, + }, + StartPos: ast.Position{Offset: 16, Line: 2, Column: 15}, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationCopy, + Pos: ast.Position{Offset: 25, Line: 2, Column: 24}, + }, + Value: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "g", + Pos: ast.Position{Offset: 27, Line: 2, Column: 26}, + }, + }, + StartPos: ast.Position{Offset: 9, Line: 2, Column: 8}, + }, + }, + result.Declarations(), + ) +} + +func TestParseReferenceTypeInVariableDeclaration(t *testing.T) { + + t.Parallel() + + const code = ` + let x: &[&R] = 1 + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.VariableDeclaration{ + Access: ast.AccessNotSpecified, + IsConstant: true, + Identifier: ast.Identifier{ + Identifier: "x", + Pos: ast.Position{Offset: 12, Line: 2, Column: 11}, + }, + TypeAnnotation: &ast.TypeAnnotation{ + IsResource: false, + Type: &ast.ReferenceType{ + Type: &ast.VariableSizedType{ + Type: &ast.ReferenceType{ + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "R", + Pos: ast.Position{Offset: 18, Line: 2, Column: 17}, + }, + }, + StartPos: ast.Position{Offset: 17, Line: 2, Column: 16}, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 16, Line: 2, Column: 15}, + EndPos: ast.Position{Offset: 19, Line: 2, Column: 18}, + }, + }, + StartPos: ast.Position{Offset: 15, Line: 2, Column: 14}, + }, + StartPos: ast.Position{Offset: 15, Line: 2, Column: 14}, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationCopy, + Pos: ast.Position{Offset: 21, Line: 2, Column: 20}, + }, + Value: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 23, Line: 2, Column: 22}, + EndPos: ast.Position{Offset: 23, Line: 2, Column: 22}, + }, + }, + StartPos: ast.Position{Offset: 8, Line: 2, Column: 7}, + }, + }, + result.Declarations(), + ) +} + +func TestParseOptionalReference(t *testing.T) { + + t.Parallel() + + const code = ` + let x: &R? = 1 + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.VariableDeclaration{ + Access: ast.AccessNotSpecified, + IsConstant: true, + Identifier: ast.Identifier{ + Identifier: "x", + Pos: ast.Position{Offset: 12, Line: 2, Column: 11}, + }, + TypeAnnotation: &ast.TypeAnnotation{ + IsResource: false, + Type: &ast.OptionalType{ + Type: &ast.ReferenceType{ + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "R", + Pos: ast.Position{Offset: 16, Line: 2, Column: 15}, + }, + }, + StartPos: ast.Position{Offset: 15, Line: 2, Column: 14}, + }, + EndPos: ast.Position{Offset: 17, Line: 2, Column: 16}, + }, + StartPos: ast.Position{Offset: 15, Line: 2, Column: 14}, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationCopy, + Pos: ast.Position{Offset: 19, Line: 2, Column: 18}, + }, + Value: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 21, Line: 2, Column: 20}, + EndPos: ast.Position{Offset: 21, Line: 2, Column: 20}, + }, + }, + StartPos: ast.Position{Offset: 8, Line: 2, Column: 7}, + }, + }, + result.Declarations(), + ) +} + +func TestParseRestrictedReferenceTypeWithBaseType(t *testing.T) { + + t.Parallel() + + const code = ` + let x: &R{I} = 1 + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.VariableDeclaration{ + Access: ast.AccessNotSpecified, + IsConstant: true, + Identifier: ast.Identifier{ + Identifier: "x", + Pos: ast.Position{Offset: 12, Line: 2, Column: 11}, + }, + TypeAnnotation: &ast.TypeAnnotation{ + IsResource: false, + Type: &ast.ReferenceType{ + Type: &ast.IntersectionType{ + Types: []*ast.NominalType{ + { + Identifier: ast.Identifier{ + Identifier: "I", + Pos: ast.Position{Offset: 18, Line: 2, Column: 17}, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 16, Line: 2, Column: 15}, + EndPos: ast.Position{Offset: 19, Line: 2, Column: 18}, + }, + }, + StartPos: ast.Position{Offset: 15, Line: 2, Column: 14}, + }, + StartPos: ast.Position{Offset: 15, Line: 2, Column: 14}, + }, + Value: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 23, Line: 2, Column: 22}, + EndPos: ast.Position{Offset: 23, Line: 2, Column: 22}, + }, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationCopy, + Pos: ast.Position{Offset: 21, Line: 2, Column: 20}, + }, + StartPos: ast.Position{Offset: 8, Line: 2, Column: 7}, + }, + }, + result.Declarations(), + ) +} + +func TestParseRestrictedReferenceTypeWithoutBaseType(t *testing.T) { + + t.Parallel() + + const code = ` + let x: &{I} = 1 + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.VariableDeclaration{ + Access: ast.AccessNotSpecified, + IsConstant: true, + Identifier: ast.Identifier{ + Identifier: "x", + Pos: ast.Position{Offset: 12, Line: 2, Column: 11}, + }, + TypeAnnotation: &ast.TypeAnnotation{ + IsResource: false, + Type: &ast.ReferenceType{ + Type: &ast.IntersectionType{ + Types: []*ast.NominalType{ + { + Identifier: ast.Identifier{ + Identifier: "I", + Pos: ast.Position{Offset: 17, Line: 2, Column: 16}, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 16, Line: 2, Column: 15}, + EndPos: ast.Position{Offset: 18, Line: 2, Column: 17}, + }, + }, + StartPos: ast.Position{Offset: 15, Line: 2, Column: 14}, + }, + StartPos: ast.Position{Offset: 15, Line: 2, Column: 14}, + }, + Value: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 22, Line: 2, Column: 21}, + EndPos: ast.Position{Offset: 22, Line: 2, Column: 21}, + }, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationCopy, + Pos: ast.Position{Offset: 20, Line: 2, Column: 19}, + }, + StartPos: ast.Position{Offset: 8, Line: 2, Column: 7}, + }, + }, + result.Declarations(), + ) +} + +func TestParseOptionalRestrictedType(t *testing.T) { + + t.Parallel() + + const code = ` + let x: @R{I}? = 1 + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.VariableDeclaration{ + Access: ast.AccessNotSpecified, + IsConstant: true, + Identifier: ast.Identifier{ + Identifier: "x", + Pos: ast.Position{Offset: 12, Line: 2, Column: 11}, + }, + TypeAnnotation: &ast.TypeAnnotation{ + IsResource: true, + Type: &ast.OptionalType{ + Type: &ast.IntersectionType{ + Types: []*ast.NominalType{ + { + Identifier: ast.Identifier{ + Identifier: "I", + Pos: ast.Position{Offset: 18, Line: 2, Column: 17}, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 16, Line: 2, Column: 15}, + EndPos: ast.Position{Offset: 19, Line: 2, Column: 18}, + }, + }, + EndPos: ast.Position{Offset: 20, Line: 2, Column: 19}, + }, + StartPos: ast.Position{Offset: 15, Line: 2, Column: 14}, + }, + Value: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 24, Line: 2, Column: 23}, + EndPos: ast.Position{Offset: 24, Line: 2, Column: 23}, + }, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationCopy, + Pos: ast.Position{Offset: 22, Line: 2, Column: 21}, + }, + StartPos: ast.Position{Offset: 8, Line: 2, Column: 7}, + }, + }, + result.Declarations(), + ) +} + +func TestParseOptionalRestrictedTypeOnlyRestrictions(t *testing.T) { + + t.Parallel() + + const code = ` + let x: @{I}? = 1 + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.VariableDeclaration{ + Access: ast.AccessNotSpecified, + IsConstant: true, + Identifier: ast.Identifier{ + Identifier: "x", + Pos: ast.Position{Offset: 12, Line: 2, Column: 11}, + }, + TypeAnnotation: &ast.TypeAnnotation{ + IsResource: true, + Type: &ast.OptionalType{ + Type: &ast.IntersectionType{ + Types: []*ast.NominalType{ + { + Identifier: ast.Identifier{ + Identifier: "I", + Pos: ast.Position{Offset: 17, Line: 2, Column: 16}, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{Offset: 16, Line: 2, Column: 15}, + EndPos: ast.Position{Offset: 18, Line: 2, Column: 17}, + }, + }, + EndPos: ast.Position{Offset: 19, Line: 2, Column: 18}, + }, + StartPos: ast.Position{Offset: 15, Line: 2, Column: 14}, + }, + Value: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 23, Line: 2, Column: 22}, + EndPos: ast.Position{Offset: 23, Line: 2, Column: 22}, + }, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationCopy, + Pos: ast.Position{Offset: 21, Line: 2, Column: 20}, + }, + StartPos: ast.Position{Offset: 8, Line: 2, Column: 7}, + }, + }, + result.Declarations(), + ) +} + +func TestParseAuthorizedReferenceType(t *testing.T) { + + t.Parallel() + + const code = ` + let x: auth &R = 1 + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.VariableDeclaration{ + Access: ast.AccessNotSpecified, + IsConstant: true, + Identifier: ast.Identifier{ + Identifier: "x", + Pos: ast.Position{Offset: 12, Line: 2, Column: 11}, + }, + TypeAnnotation: &ast.TypeAnnotation{ + IsResource: false, + Type: &ast.ReferenceType{ + Authorization: nil, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "R", Pos: ast.Position{Offset: 21, Line: 2, Column: 20}}, + }, + StartPos: ast.Position{Offset: 15, Line: 2, Column: 14}, + }, + StartPos: ast.Position{Offset: 15, Line: 2, Column: 14}, + }, + Value: &ast.IntegerExpression{ + PositiveLiteral: []byte("1"), + Value: big.NewInt(1), + Base: 10, + Range: ast.Range{ + StartPos: ast.Position{Offset: 25, Line: 2, Column: 24}, + EndPos: ast.Position{Offset: 25, Line: 2, Column: 24}, + }, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationCopy, + Pos: ast.Position{Offset: 23, Line: 2, Column: 22}, + }, + StartPos: ast.Position{Offset: 8, Line: 2, Column: 7}, + SecondTransfer: nil, + SecondValue: nil, + ParentIfStatement: nil, + }, + }, + result.Declarations(), + ) +} + +func TestParseInstantiationTypeInVariableDeclaration(t *testing.T) { + + t.Parallel() + + const code = ` + let a: MyContract.MyStruct = b + ` + result, errs := testParseProgram(code) + require.Empty(t, errs) + + utils.AssertEqualWithDiff(t, + []ast.Declaration{ + &ast.VariableDeclaration{ + Access: ast.AccessNotSpecified, + IsConstant: true, + Identifier: ast.Identifier{ + Identifier: "a", + Pos: ast.Position{Offset: 11, Line: 2, Column: 10}, + }, + TypeAnnotation: &ast.TypeAnnotation{ + IsResource: false, + Type: &ast.InstantiationType{ + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "MyContract", + Pos: ast.Position{Offset: 14, Line: 2, Column: 13}, + }, + NestedIdentifiers: []ast.Identifier{ + { + Identifier: "MyStruct", + Pos: ast.Position{Offset: 25, Line: 2, Column: 24}, + }, + }, + }, + TypeArguments: []*ast.TypeAnnotation{ + { + IsResource: false, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "Int", + Pos: ast.Position{Offset: 34, Line: 2, Column: 33}, + }, + }, + StartPos: ast.Position{Offset: 34, Line: 2, Column: 33}, + }, + { + IsResource: true, + Type: &ast.NominalType{ + Identifier: ast.Identifier{ + Identifier: "R", + Pos: ast.Position{Offset: 40, Line: 2, Column: 39}, + }, + }, + StartPos: ast.Position{Offset: 39, Line: 2, Column: 38}, + }, + }, + TypeArgumentsStartPos: ast.Position{Offset: 33, Line: 2, Column: 32}, + EndPos: ast.Position{Offset: 42, Line: 2, Column: 41}, + }, + StartPos: ast.Position{Offset: 14, Line: 2, Column: 13}, + }, + Transfer: &ast.Transfer{ + Operation: ast.TransferOperationCopy, + Pos: ast.Position{Offset: 44, Line: 2, Column: 43}, + }, + Value: &ast.IdentifierExpression{ + Identifier: ast.Identifier{ + Identifier: "b", + Pos: ast.Position{Offset: 46, Line: 2, Column: 45}, + }, + }, + StartPos: ast.Position{Offset: 7, Line: 2, Column: 6}, + }, + }, + result.Declarations(), + ) +} + +func TestParseConstantSizedSizedArrayWithTrailingUnderscoreSize(t *testing.T) { + + t.Parallel() + + _, errs := testParseDeclarations(` + let T:[d;0_]=0 + `) + + utils.AssertEqualWithDiff(t, + []error{ + &InvalidIntegerLiteralError{ + Literal: "0_", + IntegerLiteralKind: common.IntegerLiteralKindDecimal, + InvalidIntegerLiteralKind: InvalidNumberLiteralKindTrailingUnderscore, + Range: ast.Range{ + StartPos: ast.Position{Line: 2, Column: 12, Offset: 13}, + EndPos: ast.Position{Line: 2, Column: 13, Offset: 14}, + }, + }, + }, + errs, + ) +} From 45a158d106772be80960911198363022f1a92fbc Mon Sep 17 00:00:00 2001 From: Daniel Sainati Date: Tue, 23 Jan 2024 12:50:03 -0500 Subject: [PATCH 2/5] add deprecated comment --- runtime/ast/access.go | 2 +- runtime/common/declarationkind.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/runtime/ast/access.go b/runtime/ast/access.go index 123fda02fa..f1f5d54a88 100644 --- a/runtime/ast/access.go +++ b/runtime/ast/access.go @@ -215,7 +215,7 @@ const ( AccessContract AccessAccount AccessAll - PubSettableLegacy + PubSettableLegacy // Deprecated ) func PrimitiveAccessCount() int { diff --git a/runtime/common/declarationkind.go b/runtime/common/declarationkind.go index ccdd061b3b..a7d7942a28 100644 --- a/runtime/common/declarationkind.go +++ b/runtime/common/declarationkind.go @@ -176,7 +176,7 @@ func (k DeclarationKind) Keywords() string { return "event" case DeclarationKindInitializer: return "init" - case DeclarationKindDestructorLegacy: + case DeclarationKindDestructorLegacy: // Deprecated return "destroy" case DeclarationKindAttachment: return "attachment" From 6c6947ca7b24591700760b6aef8723a3822db0da Mon Sep 17 00:00:00 2001 From: Daniel Sainati Date: Tue, 23 Jan 2024 12:58:50 -0500 Subject: [PATCH 3/5] add legacy restricted type field --- runtime/ast/type.go | 3 ++- runtime/ast/type_test.go | 1 + runtime/old_parser/type.go | 4 +++- runtime/old_parser/type_test.go | 35 +++++++++++++++++++++++++++++++++ 4 files changed, 41 insertions(+), 2 deletions(-) diff --git a/runtime/ast/type.go b/runtime/ast/type.go index b56b6c744f..98c10704a5 100644 --- a/runtime/ast/type.go +++ b/runtime/ast/type.go @@ -634,7 +634,8 @@ func (t *ReferenceType) CheckEqual(other Type, checker TypeEqualityChecker) erro // IntersectionType type IntersectionType struct { - Types []*NominalType + LegacyRestrictedType Type // Deprecated + Types []*NominalType Range } diff --git a/runtime/ast/type_test.go b/runtime/ast/type_test.go index aaaaa2b1e6..8269905aa4 100644 --- a/runtime/ast/type_test.go +++ b/runtime/ast/type_test.go @@ -1347,6 +1347,7 @@ func TestIntersectionType_MarshalJSON(t *testing.T) { ` { "Type": "IntersectionType", + "LegacyRestrictedType": null, "Types": [ { "Type": "NominalType", diff --git a/runtime/old_parser/type.go b/runtime/old_parser/type.go index ba451cc861..cbd55bec80 100644 --- a/runtime/old_parser/type.go +++ b/runtime/old_parser/type.go @@ -543,7 +543,7 @@ func defineRestrictedOrDictionaryType() { // Skip the closing brace p.next() - result = ast.NewIntersectionType( + intersection := ast.NewIntersectionType( p.memoryGauge, nominalTypes, ast.NewRange( @@ -552,6 +552,8 @@ func defineRestrictedOrDictionaryType() { endPos, ), ) + intersection.LegacyRestrictedType = left + result = intersection return result, err, false }, diff --git a/runtime/old_parser/type_test.go b/runtime/old_parser/type_test.go index e83483bf49..f9463360e2 100644 --- a/runtime/old_parser/type_test.go +++ b/runtime/old_parser/type_test.go @@ -364,6 +364,13 @@ func TestParseRestrictedType(t *testing.T) { utils.AssertEqualWithDiff(t, &ast.IntersectionType{ + LegacyRestrictedType: &ast.NominalType{ + NestedIdentifiers: nil, + Identifier: ast.Identifier{ + Identifier: "T", + Pos: ast.Position{Offset: 0, Line: 1, Column: 0}, + }, + }, Range: ast.Range{ StartPos: ast.Position{Line: 1, Column: 0, Offset: 0}, EndPos: ast.Position{Line: 1, Column: 2, Offset: 2}, @@ -382,6 +389,13 @@ func TestParseRestrictedType(t *testing.T) { utils.AssertEqualWithDiff(t, &ast.IntersectionType{ + LegacyRestrictedType: &ast.NominalType{ + NestedIdentifiers: nil, + Identifier: ast.Identifier{ + Identifier: "T", + Pos: ast.Position{Offset: 0, Line: 1, Column: 0}, + }, + }, Types: []*ast.NominalType{ { Identifier: ast.Identifier{ @@ -408,6 +422,13 @@ func TestParseRestrictedType(t *testing.T) { utils.AssertEqualWithDiff(t, &ast.IntersectionType{ + LegacyRestrictedType: &ast.NominalType{ + NestedIdentifiers: nil, + Identifier: ast.Identifier{ + Identifier: "T", + Pos: ast.Position{Offset: 0, Line: 1, Column: 0}, + }, + }, Types: []*ast.NominalType{ { Identifier: ast.Identifier{ @@ -2465,6 +2486,13 @@ func TestParseRestrictedReferenceTypeWithBaseType(t *testing.T) { IsResource: false, Type: &ast.ReferenceType{ Type: &ast.IntersectionType{ + LegacyRestrictedType: &ast.NominalType{ + NestedIdentifiers: nil, + Identifier: ast.Identifier{ + Identifier: "R", + Pos: ast.Position{Offset: 16, Line: 2, Column: 15}, + }, + }, Types: []*ast.NominalType{ { Identifier: ast.Identifier{ @@ -2585,6 +2613,13 @@ func TestParseOptionalRestrictedType(t *testing.T) { IsResource: true, Type: &ast.OptionalType{ Type: &ast.IntersectionType{ + LegacyRestrictedType: &ast.NominalType{ + NestedIdentifiers: nil, + Identifier: ast.Identifier{ + Identifier: "R", + Pos: ast.Position{Offset: 16, Line: 2, Column: 15}, + }, + }, Types: []*ast.NominalType{ { Identifier: ast.Identifier{ From 45a736ae9f4be36755120b23b16dcebe51f8cdec Mon Sep 17 00:00:00 2001 From: Daniel Sainati Date: Tue, 23 Jan 2024 14:52:56 -0500 Subject: [PATCH 4/5] fix tests --- runtime/ast/primitiveaccess_string.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/runtime/ast/primitiveaccess_string.go b/runtime/ast/primitiveaccess_string.go index 40ebd6decd..5f29644c90 100644 --- a/runtime/ast/primitiveaccess_string.go +++ b/runtime/ast/primitiveaccess_string.go @@ -13,11 +13,12 @@ func _() { _ = x[AccessContract-2] _ = x[AccessAccount-3] _ = x[AccessAll-4] + _ = x[PubSettableLegacy-5] } -const _PrimitiveAccess_name = "AccessNotSpecifiedAccessSelfAccessContractAccessAccountAccessAll" +const _PrimitiveAccess_name = "AccessNotSpecifiedAccessSelfAccessContractAccessAccountAccessAllPubSettableLegacy" -var _PrimitiveAccess_index = [...]uint8{0, 18, 28, 42, 55, 64} +var _PrimitiveAccess_index = [...]uint8{0, 18, 28, 42, 55, 64, 81} func (i PrimitiveAccess) String() string { if i >= PrimitiveAccess(len(_PrimitiveAccess_index)-1) { From 110bd77514aaab13d50cb9eab935e85c026b4da7 Mon Sep 17 00:00:00 2001 From: Daniel Sainati Date: Tue, 23 Jan 2024 16:00:47 -0500 Subject: [PATCH 5/5] add legacyAuthorized field to reference types --- runtime/ast/access.go | 6 +++--- runtime/ast/primitiveaccess_string.go | 6 +++--- runtime/ast/type.go | 7 ++++--- runtime/ast/type_test.go | 1 + runtime/old_parser/declaration.go | 2 +- runtime/old_parser/declaration_test.go | 10 +++++----- runtime/old_parser/type.go | 7 +++++-- runtime/old_parser/type_test.go | 6 ++++-- 8 files changed, 26 insertions(+), 19 deletions(-) diff --git a/runtime/ast/access.go b/runtime/ast/access.go index f1f5d54a88..1f90a982d7 100644 --- a/runtime/ast/access.go +++ b/runtime/ast/access.go @@ -215,7 +215,7 @@ const ( AccessContract AccessAccount AccessAll - PubSettableLegacy // Deprecated + AccessPubSettableLegacy // Deprecated ) func PrimitiveAccessCount() int { @@ -254,7 +254,7 @@ func (a PrimitiveAccess) Keyword() string { return "access(account)" case AccessContract: return "access(contract)" - case PubSettableLegacy: + case AccessPubSettableLegacy: return "pub(set)" } @@ -273,7 +273,7 @@ func (a PrimitiveAccess) Description() string { return "account" case AccessContract: return "contract" - case PubSettableLegacy: + case AccessPubSettableLegacy: return "legacy public settable" } diff --git a/runtime/ast/primitiveaccess_string.go b/runtime/ast/primitiveaccess_string.go index 5f29644c90..03b4adf583 100644 --- a/runtime/ast/primitiveaccess_string.go +++ b/runtime/ast/primitiveaccess_string.go @@ -13,12 +13,12 @@ func _() { _ = x[AccessContract-2] _ = x[AccessAccount-3] _ = x[AccessAll-4] - _ = x[PubSettableLegacy-5] + _ = x[AccessPubSettableLegacy-5] } -const _PrimitiveAccess_name = "AccessNotSpecifiedAccessSelfAccessContractAccessAccountAccessAllPubSettableLegacy" +const _PrimitiveAccess_name = "AccessNotSpecifiedAccessSelfAccessContractAccessAccountAccessAllAccessPubSettableLegacy" -var _PrimitiveAccess_index = [...]uint8{0, 18, 28, 42, 55, 64, 81} +var _PrimitiveAccess_index = [...]uint8{0, 18, 28, 42, 55, 64, 87} func (i PrimitiveAccess) String() string { if i >= PrimitiveAccess(len(_PrimitiveAccess_index)-1) { diff --git a/runtime/ast/type.go b/runtime/ast/type.go index 98c10704a5..4d9f48c893 100644 --- a/runtime/ast/type.go +++ b/runtime/ast/type.go @@ -537,9 +537,10 @@ func (t *FunctionType) CheckEqual(other Type, checker TypeEqualityChecker) error // ReferenceType type ReferenceType struct { - Type Type `json:"ReferencedType"` - StartPos Position `json:"-"` - Authorization Authorization `json:"Authorization"` + Type Type `json:"ReferencedType"` + StartPos Position `json:"-"` + LegacyAuthorized bool + Authorization Authorization `json:"Authorization"` } var _ Type = &ReferenceType{} diff --git a/runtime/ast/type_test.go b/runtime/ast/type_test.go index 8269905aa4..df94807b22 100644 --- a/runtime/ast/type_test.go +++ b/runtime/ast/type_test.go @@ -1205,6 +1205,7 @@ func TestReferenceType_MarshalJSON(t *testing.T) { ` { "Type": "ReferenceType", + "LegacyAuthorized": false, "Authorization": { "ConjunctiveElements": [ { diff --git a/runtime/old_parser/declaration.go b/runtime/old_parser/declaration.go index 2edab477fb..34981591b1 100644 --- a/runtime/old_parser/declaration.go +++ b/runtime/old_parser/declaration.go @@ -273,7 +273,7 @@ func parseAccess(p *parser) (ast.PrimitiveAccess, error) { return ast.AccessNotSpecified, err } - return ast.PubSettableLegacy, nil + return ast.AccessPubSettableLegacy, nil case keywordAccess: // Skip the `access` keyword diff --git a/runtime/old_parser/declaration_test.go b/runtime/old_parser/declaration_test.go index 909c626f2f..69fdea99a5 100644 --- a/runtime/old_parser/declaration_test.go +++ b/runtime/old_parser/declaration_test.go @@ -1531,7 +1531,7 @@ func TestParseAccess(t *testing.T) { require.Empty(t, errs) utils.AssertEqualWithDiff(t, - ast.PubSettableLegacy, + ast.AccessPubSettableLegacy, result, ) }) @@ -2648,7 +2648,7 @@ func TestParseCompositeDeclaration(t *testing.T) { Members: ast.NewUnmeteredMembers( []ast.Declaration{ &ast.FieldDeclaration{ - Access: ast.PubSettableLegacy, + Access: ast.AccessPubSettableLegacy, VariableKind: ast.VariableKindVariable, Identifier: ast.Identifier{ Identifier: "foo", @@ -3031,7 +3031,7 @@ func TestParseAttachmentDeclaration(t *testing.T) { Members: ast.NewUnmeteredMembers( []ast.Declaration{ &ast.FieldDeclaration{ - Access: ast.PubSettableLegacy, + Access: ast.AccessPubSettableLegacy, VariableKind: ast.VariableKindVariable, Identifier: ast.Identifier{ Identifier: "foo", @@ -3233,7 +3233,7 @@ func TestParseInterfaceDeclaration(t *testing.T) { Members: ast.NewUnmeteredMembers( []ast.Declaration{ &ast.FieldDeclaration{ - Access: ast.PubSettableLegacy, + Access: ast.AccessPubSettableLegacy, VariableKind: ast.VariableKindVariable, Identifier: ast.Identifier{ Identifier: "foo", @@ -4423,7 +4423,7 @@ func TestParseStructure(t *testing.T) { Members: ast.NewUnmeteredMembers( []ast.Declaration{ &ast.FieldDeclaration{ - Access: ast.PubSettableLegacy, + Access: ast.AccessPubSettableLegacy, VariableKind: ast.VariableKindVariable, Identifier: ast.Identifier{ Identifier: "foo", diff --git a/runtime/old_parser/type.go b/runtime/old_parser/type.go index cbd55bec80..2623d4a10b 100644 --- a/runtime/old_parser/type.go +++ b/runtime/old_parser/type.go @@ -169,12 +169,15 @@ func init() { return nil, err } - return ast.NewReferenceType( + refType := ast.NewReferenceType( p.memoryGauge, nil, right, token.StartPos, - ), nil + ) + refType.LegacyAuthorized = true + + return refType, nil default: return parseNominalTypeRemainder(p, token) diff --git a/runtime/old_parser/type_test.go b/runtime/old_parser/type_test.go index f9463360e2..490d1ed35f 100644 --- a/runtime/old_parser/type_test.go +++ b/runtime/old_parser/type_test.go @@ -307,7 +307,8 @@ func TestParseReferenceType(t *testing.T) { utils.AssertEqualWithDiff(t, &ast.ReferenceType{ - Authorization: nil, + Authorization: nil, + LegacyAuthorized: true, Type: &ast.NominalType{ Identifier: ast.Identifier{ Identifier: "Int", @@ -2739,7 +2740,8 @@ func TestParseAuthorizedReferenceType(t *testing.T) { TypeAnnotation: &ast.TypeAnnotation{ IsResource: false, Type: &ast.ReferenceType{ - Authorization: nil, + LegacyAuthorized: true, + Authorization: nil, Type: &ast.NominalType{ Identifier: ast.Identifier{ Identifier: "R", Pos: ast.Position{Offset: 21, Line: 2, Column: 20}},