Support lexing and parsing positional params (#7651)

This implements lexing and parsing positional parameters such as `$0`.
This commit is contained in:
Özgür T. Önsoy
2026-09-03 19:06:20 +00:00
committed by GitHub
parent 27849b385c
commit 5474347d7d
14 changed files with 282 additions and 8 deletions
@@ -0,0 +1,14 @@
// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
// Exceptions. See /LICENSE for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
#include "toolchain/check/context.h"
#include "toolchain/check/handle.h"
#include "toolchain/parse/node_ids.h"
namespace Carbon::Check {
auto HandleParseNode(Context& context, Parse::PositionalParamExprId node_id)
-> bool {
return context.TODO(node_id, "HandlePositionalParamExprId");
}
} // namespace Carbon::Check
+5
View File
@@ -59,12 +59,16 @@ CARBON_DIAGNOSTIC_KIND(ErrorReadingFile)
// ============================================================================
CARBON_DIAGNOSTIC_KIND(InvalidRealLiteralRadix)
CARBON_DIAGNOSTIC_KIND(CharacterOnlyAllowedAtStart)
CARBON_DIAGNOSTIC_KIND(ContentBeforeStringTerminator)
CARBON_DIAGNOSTIC_KIND(DecimalEscapeSequence)
CARBON_DIAGNOSTIC_KIND(DollarIntLiteralLeadingZero)
CARBON_DIAGNOSTIC_KIND(DollarIntLiteralMissingNumber)
CARBON_DIAGNOSTIC_KIND(DumpSemIRRangeMissingEnd)
CARBON_DIAGNOSTIC_KIND(DumpSemIRRangeMissingBegin)
CARBON_DIAGNOSTIC_KIND(EmptyDigitSequence)
CARBON_DIAGNOSTIC_KIND(HexadecimalEscapeMissingDigits)
CARBON_DIAGNOSTIC_KIND(InvalidCharacterInDollarIntLiteral)
CARBON_DIAGNOSTIC_KIND(InvalidDigit)
CARBON_DIAGNOSTIC_KIND(InvalidDigitSeparator)
CARBON_DIAGNOSTIC_KIND(InvalidHorizontalWhitespaceInString)
@@ -75,6 +79,7 @@ CARBON_DIAGNOSTIC_KIND(NoWhitespaceAfterCommentIntroducer)
CARBON_DIAGNOSTIC_KIND(TooManyDigits)
CARBON_DIAGNOSTIC_KIND(TooManyTokens)
CARBON_DIAGNOSTIC_KIND(TooManyTypeBitWidthDigits)
CARBON_DIAGNOSTIC_KIND(TooManyDollarIntDigits)
CARBON_DIAGNOSTIC_KIND(UnicodeEscapeMissingBracedDigits)
CARBON_DIAGNOSTIC_KIND(UnicodeEscapeSurrogate)
CARBON_DIAGNOSTIC_KIND(UnicodeEscapeTooLarge)
+109
View File
@@ -191,6 +191,11 @@ class [[clang::internal_linkage]] Lexer {
auto LexWordAsTypeLiteralToken(llvm::StringRef word, int32_t byte_offset)
-> LexResult;
// Given a lexed word, determine whether it is a dollar int literal and if so
// form the corresponding token,
auto LexWordAsDollarIntLiteralToken(llvm::StringRef word, int32_t byte_offset)
-> LexResult;
auto LexKeywordOrIdentifier(llvm::StringRef source_text, ssize_t& position)
-> LexResult;
@@ -286,6 +291,7 @@ static constexpr std::array<bool, 256> IsIdStartByteTable = [] {
table[c] = true;
}
table['_'] = true;
table['$'] = true;
return table;
}();
@@ -297,6 +303,8 @@ static constexpr std::array<bool, 256> IsIdByteTable = [] {
for (char c = '0'; c <= '9'; ++c) {
table[c] = true;
}
// Identifiers can only have `$` in start.
table['$'] = false;
return table;
}();
@@ -307,6 +315,12 @@ static constexpr std::array<bool, 256> IsIdByteTable = [] {
static auto ScanForIdentifierPrefixScalar(llvm::StringRef text, ssize_t i)
-> llvm::StringRef {
const ssize_t size = text.size();
if (i == 0 && !text.empty()) {
if (!IsIdStartByteTable[static_cast<unsigned char>(text[i])]) {
return {};
}
++i;
}
while (i < size && IsIdByteTable[static_cast<unsigned char>(text[i])]) {
++i;
}
@@ -410,6 +424,13 @@ static auto ScanForIdentifierPrefixX86(llvm::StringRef text)
// Use `ssize_t` for performance here as we index memory in a tight loop.
ssize_t i = 0;
if (!text.empty()) {
if (!IsIdStartByteTable[static_cast<unsigned char>(text[i])]) {
return {};
}
++i;
}
const ssize_t size = text.size();
while ((i + 16) <= size) {
__m128i input =
@@ -644,6 +665,7 @@ static constexpr auto MakeDispatchTable() -> DispatchTableT {
table['/'] = &DispatchLexCommentOrSlash;
table['_'] = &DispatchLexKeywordOrIdentifier;
table['$'] = &DispatchLexKeywordOrIdentifier;
// Note that we don't use `llvm::seq` because this needs to be `constexpr`
// evaluated.
for (unsigned char c = 'a'; c <= 'z'; ++c) {
@@ -1453,6 +1475,89 @@ auto Lexer::LexWordAsTypeLiteralToken(llvm::StringRef word, int32_t byte_offset)
return LexTokenWithPayload(kind, bit_width_payload, byte_offset);
}
auto Lexer::LexWordAsDollarIntLiteralToken(llvm::StringRef word,
int32_t byte_offset) -> LexResult {
if (!word.starts_with('$')) {
return LexResult::NoMatch();
}
if (!has_leading_space_) {
auto prev_token = buffer_.tokens().end()[-1];
auto kind = buffer_.GetKind(prev_token);
if (kind.is_word()) {
CARBON_DIAGNOSTIC(
CharacterOnlyAllowedAtStart, Error,
"`$` is only allowed at the start of a positional parameter");
emitter_.Emit(word.begin(), CharacterOnlyAllowedAtStart);
auto& prev_token_info = buffer_.token_infos_.Get(prev_token);
auto prev_token_text_size = buffer_.GetTokenText(prev_token).size();
prev_token_info = TokenInfo(TokenKind::Error, has_leading_space_,
prev_token_text_size + word.size(),
prev_token_info.byte_offset());
return LexResult(TokenIndex(buffer_.token_infos_.size() - 1));
}
}
auto diagnose_invalid_char = [&]() {
CARBON_DIAGNOSTIC(
InvalidCharacterInDollarIntLiteral, Error,
"Positional parameters can only contain digits after `$`");
emitter_.Emit(word.begin() + 1, InvalidCharacterInDollarIntLiteral);
return LexTokenWithPayload(TokenKind::Error, word.size(), byte_offset);
};
if (word.size() < 2) {
CARBON_DIAGNOSTIC(DollarIntLiteralMissingNumber, Error,
"Expected digits after `$`");
emitter_.Emit(word.begin() + 1, DollarIntLiteralMissingNumber);
return LexTokenWithPayload(TokenKind::Error, word.size(), byte_offset);
}
if (word[1] == '0' && word.size() > 2) {
CARBON_DIAGNOSTIC(
DollarIntLiteralLeadingZero, Error,
"Leading zeroes are not allowed in positional parameters");
emitter_.Emit(word.begin() + 1, DollarIntLiteralLeadingZero);
return LexTokenWithPayload(TokenKind::Error, word.size(), byte_offset);
}
if ((word[1] < '0' || word[1] > '9')) {
return diagnose_invalid_char();
}
auto suffix = word.substr(1);
int64_t suffix_value;
constexpr ssize_t DigitLimit =
std::numeric_limits<decltype(suffix_value)>::digits10;
if (suffix.size() > DigitLimit) {
// See if this is not actually a dollar int literal.
if (!llvm::all_of(suffix, IsDecimalDigit)) {
return diagnose_invalid_char();
}
// Otherwise, diagnose and produce an error token.
CARBON_DIAGNOSTIC(TooManyDollarIntDigits, Error,
"found a positional parameter using {0} digits, "
"which is greater than the limit of {1}",
size_t, size_t);
emitter_.Emit(word.begin() + 1, TooManyDollarIntDigits, suffix.size(),
DigitLimit);
return LexTokenWithPayload(TokenKind::Error, word.size(), byte_offset);
}
suffix_value = suffix[0] - '0';
for (char c : suffix.drop_front()) {
if (!IsDecimalDigit(c)) {
return diagnose_invalid_char();
}
suffix_value = suffix_value * 10 + (c - '0');
}
CARBON_CHECK(suffix_value >= 0);
return LexTokenWithPayload(
TokenKind::DollarIntLiteral,
buffer_.value_stores_->ints().Add(suffix_value).AsTokenPayload(),
byte_offset);
}
auto Lexer::LexKeywordOrIdentifier(llvm::StringRef source_text,
ssize_t& position) -> LexResult {
if (static_cast<unsigned char>(source_text[position]) > 0x7F) {
@@ -1476,6 +1581,10 @@ auto Lexer::LexKeywordOrIdentifier(llvm::StringRef source_text,
LexWordAsTypeLiteralToken(identifier_text, byte_offset)) {
return result;
}
if (LexResult result =
LexWordAsDollarIntLiteralToken(identifier_text, byte_offset)) {
return result;
}
// Check if the text matches a keyword token, and if so use that.
TokenKind kind = llvm::StringSwitch<TokenKind>(identifier_text)
+1 -1
View File
@@ -42,7 +42,7 @@ auto NumericLiteral::Lex(llvm::StringRef source_text,
int n = source_text.size();
for (; i != n; ++i) {
char c = source_text[i];
if (IsAlnum(c) || c == '_') {
if (IsAlnum(c) || c == '_' || c == '$') {
if (IsLower(c) && seen_radix_point && !seen_plus_minus) {
result.exponent_ = i;
seen_potential_exponent = true;
+101
View File
@@ -0,0 +1,101 @@
// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
// Exceptions. See /LICENSE for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
// AUTOUPDATE
// TIP: To test this file alone, run:
// TIP: bazel test //toolchain/testing:file_test --test_arg=--file_tests=toolchain/lex/testdata/dollar_int_literals.carbon
// TIP: To dump output, run:
// TIP: bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/lex/testdata/dollar_int_literals.carbon
// --- valid.carbon
// CHECK:STDOUT: - filename: valid.carbon
// CHECK:STDOUT: tokens:
$0
// CHECK:STDOUT: - { index: 1, kind: "DollarIntLiteral", line: {{ *}}[[@LINE-1]], column: 1, indent: 1, spelling: "$0", has_leading_space: true }
$1
// CHECK:STDOUT: - { index: 2, kind: "DollarIntLiteral", line: {{ *}}[[@LINE-1]], column: 1, indent: 1, spelling: "$1", has_leading_space: true }
$42
// CHECK:STDOUT: - { index: 3, kind: "DollarIntLiteral", line: {{ *}}[[@LINE-1]], column: 1, indent: 1, spelling: "$42", has_leading_space: true }
$100
// CHECK:STDOUT: - { index: 4, kind: "DollarIntLiteral", line: {{ *}}[[@LINE-1]], column: 1, indent: 1, spelling: "$100", has_leading_space: true }
$999999999999
// CHECK:STDOUT: - { index: 5, kind: "DollarIntLiteral", line: {{ *}}[[@LINE-1]], column: 1, indent: 1, spelling: "$999999999999", has_leading_space: true }
// --- fail_invalid.carbon
// CHECK:STDOUT: - filename: fail_invalid.carbon
// CHECK:STDOUT: tokens:
// CHECK:STDERR: fail_invalid.carbon:[[@LINE+4]]:2: error: Expected digits after `$` [DollarIntLiteralMissingNumber]
// CHECK:STDERR: $
// CHECK:STDERR: ^
// CHECK:STDERR:
$
// CHECK:STDOUT: - { index: 1, kind: "Error", line: {{ *}}[[@LINE-1]], column: 1, indent: 1, spelling: "$", has_leading_space: true }
// CHECK:STDERR: fail_invalid.carbon:[[@LINE+4]]:2: error: Expected digits after `$` [DollarIntLiteralMissingNumber]
// CHECK:STDERR: $ 1
// CHECK:STDERR: ^
// CHECK:STDERR:
$ 1
// CHECK:STDOUT: - { index: 2, kind: "Error", line: {{ *}}[[@LINE-1]], column: 1, indent: 1, spelling: "$", has_leading_space: true }
// CHECK:STDOUT: - { index: 3, kind: "IntLiteral", line: {{ *}}[[@LINE-2]], column: 3, indent: 1, spelling: "1", value: "1", has_leading_space: true }
// CHECK:STDERR: fail_invalid.carbon:[[@LINE+4]]:2: error: Positional parameters can only contain digits after `$` [InvalidCharacterInDollarIntLiteral]
// CHECK:STDERR: $foo
// CHECK:STDERR: ^
// CHECK:STDERR:
$foo
// CHECK:STDOUT: - { index: 4, kind: "Error", line: {{ *}}[[@LINE-1]], column: 1, indent: 1, spelling: "$foo", has_leading_space: true }
// CHECK:STDERR: fail_invalid.carbon:[[@LINE+4]]:2: error: Positional parameters can only contain digits after `$` [InvalidCharacterInDollarIntLiteral]
// CHECK:STDERR: $1a32
// CHECK:STDERR: ^
// CHECK:STDERR:
$1a32
// CHECK:STDOUT: - { index: 5, kind: "Error", line: {{ *}}[[@LINE-1]], column: 1, indent: 1, spelling: "$1a32", has_leading_space: true }
// CHECK:STDERR: fail_invalid.carbon:[[@LINE+4]]:2: error: Leading zeroes are not allowed in positional parameters [DollarIntLiteralLeadingZero]
// CHECK:STDERR: $00
// CHECK:STDERR: ^
// CHECK:STDERR:
$00
// CHECK:STDOUT: - { index: 6, kind: "Error", line: {{ *}}[[@LINE-1]], column: 1, indent: 1, spelling: "$00", has_leading_space: true }
// CHECK:STDERR: fail_invalid.carbon:[[@LINE+4]]:2: error: Leading zeroes are not allowed in positional parameters [DollarIntLiteralLeadingZero]
// CHECK:STDERR: $025321
// CHECK:STDERR: ^
// CHECK:STDERR:
$025321
// CHECK:STDOUT: - { index: 7, kind: "Error", line: {{ *}}[[@LINE-1]], column: 1, indent: 1, spelling: "$025321", has_leading_space: true }
// CHECK:STDERR: fail_invalid.carbon:[[@LINE+4]]:2: error: Leading zeroes are not allowed in positional parameters [DollarIntLiteralLeadingZero]
// CHECK:STDERR: $0x12
// CHECK:STDERR: ^
// CHECK:STDERR:
$0x12
// CHECK:STDOUT: - { index: 8, kind: "Error", line: {{ *}}[[@LINE-1]], column: 1, indent: 1, spelling: "$0x12", has_leading_space: true }
// CHECK:STDERR: fail_invalid.carbon:[[@LINE+4]]:2: error: Leading zeroes are not allowed in positional parameters [DollarIntLiteralLeadingZero]
// CHECK:STDERR: $0b1100
// CHECK:STDERR: ^
// CHECK:STDERR:
$0b1100
// CHECK:STDOUT: - { index: 9, kind: "Error", line: {{ *}}[[@LINE-1]], column: 1, indent: 1, spelling: "$0b1100", has_leading_space: true }
// CHECK:STDERR: fail_invalid.carbon:[[@LINE+4]]:2: error: invalid digit '$' in decimal numeric literal [InvalidDigit]
// CHECK:STDERR: 2$63
// CHECK:STDERR: ^
// CHECK:STDERR:
2$63
// CHECK:STDOUT: - { index: 10, kind: "Error", line: {{ *}}[[@LINE-1]], column: 1, indent: 1, spelling: "2$63", has_leading_space: true }
// CHECK:STDERR: fail_invalid.carbon:[[@LINE+4]]:4: error: `$` is only allowed at the start of a positional parameter [CharacterOnlyAllowedAtStart]
// CHECK:STDERR: abc$37
// CHECK:STDERR: ^
// CHECK:STDERR:
abc$37
// CHECK:STDOUT: - { index: 11, kind: "Error", line: {{ *}}[[@LINE-1]], column: 1, indent: 1, spelling: "abc$37" }
// CHECK:STDERR: fail_invalid.carbon:[[@LINE+4]]:6: error: `$` is only allowed at the start of a positional parameter [CharacterOnlyAllowedAtStart]
// CHECK:STDERR: r#foo$0
// CHECK:STDERR: ^
// CHECK:STDERR:
r#foo$0
// CHECK:STDOUT: - { index: 12, kind: "Error", line: {{ *}}[[@LINE-1]], column: 1, indent: 1, spelling: "r#foo$0" }
// CHECK:STDERR: fail_invalid.carbon:[[@LINE+4]]:2: error: found a positional parameter using 19 digits, which is greater than the limit of 18 [TooManyDollarIntDigits]
// CHECK:STDERR: $9999999999999999999
// CHECK:STDERR: ^
// CHECK:STDERR:
$9999999999999999999
// CHECK:STDOUT: - { index: 13, kind: "Error", line: {{ *}}[[@LINE-1]], column: 1, indent: 1, spelling: "$9999999999999999999", has_leading_space: true }
+2 -1
View File
@@ -94,7 +94,8 @@ class TokenInfo {
CARBON_DCHECK(kind() == TokenKind::IntLiteral ||
kind() == TokenKind::IntTypeLiteral ||
kind() == TokenKind::UnsignedIntTypeLiteral ||
kind() == TokenKind::FloatTypeLiteral);
kind() == TokenKind::FloatTypeLiteral ||
kind() == TokenKind::DollarIntLiteral);
return IntId::MakeFromTokenPayload(token_payload_);
}
+1
View File
@@ -248,6 +248,7 @@ CARBON_TOKEN(CharLiteral)
CARBON_TOKEN(IntTypeLiteral)
CARBON_TOKEN(UnsignedIntTypeLiteral)
CARBON_TOKEN(FloatTypeLiteral)
CARBON_TOKEN(DollarIntLiteral)
CARBON_TOKEN(FileStart)
CARBON_TOKEN(FileEnd)
+5
View File
@@ -86,6 +86,11 @@ class TokenKind : public CARBON_ENUM_BASE(TokenKind) {
*this == TokenKind::FloatTypeLiteral;
}
// Test whether this kind of token is a dollar int literal.
auto is_dollar_int_literal() const -> bool {
return *this == TokenKind::DollarIntLiteral;
}
// Test whether this kind of token is a word.
auto is_word() const -> bool {
return *this == TokenKind::Identifier || *this == TokenKind::Underscore ||
+2 -1
View File
@@ -93,7 +93,8 @@ auto TokenizedBuffer::GetTokenText(TokenIndex token) const -> llvm::StringRef {
// Refer back to the source text to avoid needing to reconstruct the
// spelling from the size.
if (token_info.kind().is_sized_type_literal()) {
if (token_info.kind().is_sized_type_literal() ||
token_info.kind().is_dollar_int_literal()) {
llvm::StringRef suffix = source_->text()
.substr(token_info.byte_offset() + 1)
.take_while(IsDecimalDigit);
+5 -5
View File
@@ -369,7 +369,7 @@ TEST_F(LexerTest, SplitsNumericLiteralsProperly) {
}
TEST_F(LexerTest, HandlesGarbageCharacters) {
constexpr char GarbageText[] = "$$💩-$\n$\0$12$\n\\\"\\\n\"x";
constexpr char GarbageText[] = "##💩-#\n#\0#12#\n\\\"\\\n\"x";
auto& buffer = compile_helper_.GetTokenizedBuffer(
llvm::StringRef(GarbageText, sizeof(GarbageText) - 1));
EXPECT_TRUE(buffer.has_errors());
@@ -381,16 +381,16 @@ TEST_F(LexerTest, HandlesGarbageCharacters) {
.line = 1,
.column = 1,
// 💩 takes 4 bytes, and we count column as bytes offset.
.text = llvm::StringRef("$$💩", 6)},
.text = llvm::StringRef("##💩", 6)},
{.kind = TokenKind::Minus, .line = 1, .column = 7},
{.kind = TokenKind::Error, .line = 1, .column = 8, .text = "$"},
{.kind = TokenKind::Error, .line = 1, .column = 8, .text = "#"},
// newline
{.kind = TokenKind::Error,
.line = 2,
.column = 1,
.text = llvm::StringRef("$\0$", 3)},
.text = llvm::StringRef("#\0#", 3)},
{.kind = TokenKind::IntLiteral, .line = 2, .column = 4, .text = "12"},
{.kind = TokenKind::Error, .line = 2, .column = 6, .text = "$"},
{.kind = TokenKind::Error, .line = 2, .column = 6, .text = "#"},
// newline
{.kind = TokenKind::Backslash, .line = 3, .column = 1, .text = "\\"},
{.kind = TokenKind::Error, .line = 3, .column = 2, .text = "\"\\"},
+5
View File
@@ -139,6 +139,11 @@ auto HandleExprInPostfix(Context& context) -> void {
context.PushState(state);
break;
}
case Lex::TokenKind::DollarIntLiteral: {
context.AddLeafNode(NodeKind::PositionalParamExpr, context.Consume());
context.PushState(state);
break;
}
case Lex::TokenKind::Str: {
context.AddLeafNode(NodeKind::StringTypeLiteral, context.Consume());
context.PushState(state);
+2
View File
@@ -311,6 +311,8 @@ CARBON_PARSE_NODE_KIND_EXPRESSION(PointerMemberAccessExpr)
CARBON_PARSE_NODE_KIND_EXPRESSION(IntLiteral)
CARBON_PARSE_NODE_KIND_EXPRESSION(PositionalParamExpr)
CARBON_PARSE_NODE_KIND_TOKEN_LITERAL(BoolLiteralFalse, False)
CARBON_PARSE_NODE_KIND_TOKEN_LITERAL(BoolLiteralTrue, True)
CARBON_PARSE_NODE_KIND_TOKEN_LITERAL(CharLiteral, CharLiteral)
+25
View File
@@ -18,6 +18,12 @@ fn foo(bar: i64, baz: i64) {
foo(baz, bar + baz);
}
// --- with_positional_params.carbon
fn foo {
foo($0, $42.0 + $86);
}
// --- with_return_type.carbon
fn foo() -> f64 {
@@ -116,6 +122,25 @@ fn TestRecoveryFromSpuriousEquals();
// CHECK:STDOUT: ├─FunctionDefinition '}'
// CHECK:STDOUT: ├─FileEnd ''
// CHECK:STDOUT: (root)
// CHECK:STDOUT: - filename: with_positional_params.carbon
// CHECK:STDOUT: ╭─FileStart ''
// CHECK:STDOUT: │ ╭─FunctionIntroducer 'fn'
// CHECK:STDOUT: │ ├─IdentifierNameNotBeforeSignature 'foo'
// CHECK:STDOUT: │ ╭─FunctionDefinitionStart '{'
// CHECK:STDOUT: │ │ ╭─IdentifierNameExpr 'foo'
// CHECK:STDOUT: │ │ ╭─CallExprStart '('
// CHECK:STDOUT: │ │ ├─PositionalParamExpr '$0'
// CHECK:STDOUT: │ │ ├─TupleLiteralComma ','
// CHECK:STDOUT: │ │ │ ╭─PositionalParamExpr '$42'
// CHECK:STDOUT: │ │ │ ├─IntLiteral '0'
// CHECK:STDOUT: │ │ │ ╭─MemberAccessExpr '.'
// CHECK:STDOUT: │ │ │ ├─PositionalParamExpr '$86'
// CHECK:STDOUT: │ │ ├─InfixOperatorPlus '+'
// CHECK:STDOUT: │ │ ╭─CallExpr ')'
// CHECK:STDOUT: │ ├─ExprStatement ';'
// CHECK:STDOUT: ├─FunctionDefinition '}'
// CHECK:STDOUT: ├─FileEnd ''
// CHECK:STDOUT: (root)
// CHECK:STDOUT: - filename: with_return_type.carbon
// CHECK:STDOUT: ╭─FileStart ''
// CHECK:STDOUT: │ ╭─FunctionIntroducer 'fn'
+5
View File
@@ -1837,6 +1837,11 @@ struct NamedConstraintDefinition {
Lex::CloseCurlyBraceTokenIndex token;
};
// `$0`
using PositionalParamExpr =
LeafNode<NodeKind::PositionalParamExpr, Lex::DollarIntLiteralTokenIndex,
NodeCategory::Expr>;
// ---------------------------------------------------------------------------
// A complete source file. Note that there is no corresponding parse node for