Skip to content
Permalink
Branch: master
Find file Copy path
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
2260 lines (2100 sloc) 89.1 KB
//
// Enhanced C# parser: this is designed to accept a superset of C# 6 (Enhanced C#).
// If this spews an error when given a valid C# file, it's a bug. File a bug report.
//
// EC# is more permisive than plain C#; it not only adds new syntax, but it
// liberalizes the existing syntax rules (e.g. "try Foo(); catch Bar();" is
// accepted in EC#) and it also accepts a lot of code that is not directly valid in
// C# (e.g. "struct X { Y(); }"). Such invalid code is allowed by the parser in
// case a macro might transform it later into something that makes more sense.
//
#importMacros LeMP.Prelude.Les;
#ecs;
import System;
import System.Collections.Generic;
import System.Linq;
import System.Text;
import System.Diagnostics;
import Loyc;
import Loyc.Syntax;
import Loyc.Syntax.Les;
import Loyc.Syntax.Lexing;
import Loyc.Collections;
import Loyc.Collections.Impl;
namespace Loyc.Ecs.Parser
{
using TT = TokenType;
using S = CodeSymbols;
using EP = EcsPrecedence;
// 0162=Unreachable code detected; 0642=Possibly mistaken empty statement
#rawText "\n\t#pragma warning disable 162, 642\n\t";
@[partial] class EcsParser
{
@[LL(2), Verbosity(1), AddCsLineDirectives(@false)]
LLLPG parser(laType(TokenType), terminalType(Token), matchType(int),
allowSwitch(@true), setType(HashSet!int), castLA(@false));
alias("(" = TT.LParen);
alias(")" = TT.RParen);
alias("[" = TT.LBrack);
alias("]" = TT.RBrack);
alias("{" = TT.LBrace);
alias("}" = TT.RBrace);
alias("." = TT.Dot);
alias("->" = TT.PtrArrow);
alias("::" = TT.ColonColon);
alias("?." = TT.NullDot);
alias("=" = TT.Set);
alias("??=" = TT.CompoundSet);
alias(":" = TT.Colon);
alias("@" = TT.At);
alias("``" = TT.BQString);
alias("\\" = TT.Backslash);
alias("*" = TT.Mul);
alias("/,%" = TT.DivMod);
alias("**" = TT.Power);
alias("+" = TT.Add);
alias("-" = TT.Sub);
alias("++,--" = TT.IncDec);
alias("&&" = TT.And);
alias("||,^^" = TT.OrXor);
alias("!" = TT.Not);
alias("&" = TT.AndBits);
alias("^" = TT.XorBits);
alias("|" = TT.OrBits);
alias("~" = TT.NotBits);
alias("==,!=" = TT.EqNeq);
alias("<" = TT.LT);
alias(">" = TT.GT);
alias("<=,>=" = TT.LEGE);
alias(".." = TT.DotDot);
alias("?" = TT.QuestionMark);
alias("??" = TT.NullCoalesce);
alias("=:" = TT.QuickBind);
alias(":=" = TT.QuickBindSet);
alias("==>" = TT.Forward);
alias("$" = TT.Substitute);
alias("=>" = TT.LambdaArrow);
alias("," = TT.Comma);
alias(";" = TT.Semicolon);
// Used to resolve the constructor ambiguity, in which "Foo()" could be a
// constructor declaration or a method call. _spaceName is the name of the
// current space, or #fn (S.Fn) when inside a method or constructor.
_spaceName::Symbol;
// Inside a LINQ expression, certain ContextualKeywords are to be treated
// as actual keywords. This flag enables that behavior.
_insideLinqExpr::bool;
// Used to detect a specific ContextualKeyword; `@` renders it not-a-keyword
fn Is(li::int, value::Symbol)::bool
{
var lt = LT(li);
return lt.Value == value && SourceFile.Text.TryGet(lt.StartIndex, '\0') != '@';
};
// A potential LINQ keyword that, it turns out, can be treated as an identifier
// because we are not in the context of a LINQ expression.
@[private] rule LinqKeywordAsId()::Token @{
&!{_insideLinqExpr} result:TT.LinqKeyword
};
// ---------------------------------------------------------------------
// -- Type names and complex identifiers -------------------------------
// ---------------------------------------------------------------------
// The complex identifier in EC# is a subset of the language of expressions,
// and data types are a superset of the language of complex identifiers.
// Complex identifiers can appear in the following contexts:
// - Space names: namespace Foo.Bar<$T> {...}
// (and yes, I want to support template parameters on namespaces someday)
// - Method/property names: bool global::System.IEquatable<T>.Equals(T other)
// Data types can appear in the following contexts:
// - Fields and properties: int* x { get; set; }
// - Methods declarations: int[] f(Foo<T>[,][] x);
// - Certain pseudo-functions: typeof(Foo<T[]>)
// Note that complex identifiers can contain substitution expressions,
// which, in turn, can contain expressions of any complexity, e.g.
// Foo<$(x*y(++z))>. Of course, complex identifiers also appear within
// normal expressions, but the expression grammar doesn't use the
// main "ComplexId" rule, instead it's handled somewhat separately.
// LLLPG is unaware of this overload, but when I write DataType(),
// it unwittingly invokes it.
@[public] fn DataType(afterAsOrIs::bool = false)::LNode {
brack::opt!Token;
var type = DataType(afterAsOrIs, out brack);
if (brack != null) {
Error("A type name cannot include [array dimensions]. The square brackets should be empty.");
};
return type;
};
@[recognizer(fn Scan_DataType(afterAsOrIs::bool = false))]
rule DataType(afterAsOrIs::bool, out majorDimension::opt!Token)::LNode @{
result:ComplexId()
TypeSuffixOpt(afterAsOrIs, out majorDimension, ref result)
};
// Complex identifier in non-declaration context, e.g. Foo.Bar or Foo<x, y>
@[FullLLk] // http://loyc-etc.blogspot.ca/2013/12/bogus-ambiguity-warnings-in-lllpg.html
token ComplexId(declContext::bool = false)::LNode @{ // Part of DataType
result:IdWithOptionalTypeParams(declContext)
[ // There can be only a single "externAlias::" prefix in a complex Id, with no type params.
{if (result.Calls(S.Of)) {Error("Type parameters cannot appear before '::' in a declaration or type name");};}
op:="::" rhs:=IdWithOptionalTypeParams(declContext)
{result = F.Call(S.ColonColon, result, rhs, result.Range.StartIndex, rhs.Range.EndIndex, op.StartIndex, op.EndIndex, NodeStyle.Operator);}
]?
[ op:="." rhs:=IdWithOptionalTypeParams(declContext)
{result = F.Dot(result, rhs, result.Range.StartIndex, rhs.Range.EndIndex, op.StartIndex, op.EndIndex, NodeStyle.Operator);}
]*
};
@[FullLLk]
token IdWithOptionalTypeParams(declarationContext::bool)::LNode @{
result:IdAtom [TParams(declarationContext, ref result)]?
};
// identifier, $identifier, $(expr), operator+ (or another operator name), or a primitive type (int, string)
rule IdAtom::LNode @{
{r::LNode;}
( t:="$" e:=Atom {e=AutoRemoveParens(e);}
{r = F.Call(S.Substitute, e, t.StartIndex, e.Range.EndIndex, t.StartIndex, t.EndIndex, NodeStyle.Operator);}
| op:=TT.Operator t:=AnyOperator
{r = F.Attr(_triviaUseOperatorKeyword, F.Id(t.Value -> Symbol, op.StartIndex, t.EndIndex));}
| t:=(TT.Id|TT.ContextualKeyword|TT.TypeKeyword)
{r = F.Id(t);}
| t:=LinqKeywordAsId
{r = F.Id(t);}
//| error {Error("Expected an identifier"); r = MissingHere();}
) {return r;}
};
@[#static] fn AutoRemoveParens(node::LNode)::LNode
{
i::int = node.Attrs.IndexWithName(S.TriviaInParens);
if (i > -1) {
return node.WithAttrs(node.Attrs.RemoveAt(i));
};
return node;
};
// List of type parameters. `declContext` specifies that type parameters can
// have [normal attributes] and in/out variance attrbutes.
@[recognizer(fn Scan_TParams(declContext::bool))]
rule TParams(declContext::bool, ref r::LNode)
@{
{list::VList!LNode = (new VList!LNode(r));}
{endIndex::int;}
( op:"<" [list+=TParamDeclOrDataType(declContext)
("," list+=TParamDeclOrDataType(declContext))*]? end:=">" {endIndex = end.EndIndex;}
| op:"." t:="[" end:="]" {list = AppendExprsInside(t, list); endIndex = end.EndIndex;} // Nemerle style
| op:"!" t:="(" end:=")" {list = AppendExprsInside(t, list); endIndex = end.EndIndex;} // D & LES style
| op:"!" list+=IdWithOptionalTypeParams(declContext) {endIndex = list.Last.Range.EndIndex;}
)
{
start::int = r.Range.StartIndex;
r = F.Call(S.Of, list, start, endIndex, op.StartIndex, op.EndIndex, NodeStyle.Operator);
}
};
@[recognizer(fn Scan_TParamDeclOrDataType(declarationContext::bool))]
rule TParamDeclOrDataType(declarationContext::bool)::LNode @{
{attrs::VList!LNode = default(VList!LNode); startIndex::int = GetTextPosition(InputPosition);}
( result:DataType(false)
/ &{declarationContext}
NormalAttributes(ref attrs)
TParamAttributeKeywords(ref attrs)
result:IdAtom
)
{$result = $result.WithAttrs(attrs);}
};
// This is the same as ComplexId except that it is used in declaration
// locations such as the name of a class. The difference is that the
// type parameters can have [normal attributes] and in/out variance
// attrbutes. This matcher also helps match properties and therefore
// optionally allows names ending in 'this', such as 'IList<T>.this'
fn ComplexNameDecl()::LNode { _::bool; return ComplexNameDecl(false, out _); };
@[FullLLk, recognizer(fn Scan_ComplexNameDecl(thisAllowed::bool = false))]
token ComplexNameDecl(thisAllowed::bool, out hasThis::bool)::LNode @{
result:ComplexId(declContext: true) {hasThis = false;}
[ op:="." ComplexThisDecl(thisAllowed) {hasThis = true;}
{result = F.Dot(result, $ComplexThisDecl, result.Range.StartIndex, $ComplexThisDecl.Range.EndIndex, op.StartIndex, op.EndIndex, NodeStyle.Operator);}
]?
};
// The matcher for method and property names is similar to ComplexNameDecl but
// can allow 'this' if it's a property, and 'operator true' if it's a method.
@[recognizer(fn Scan_MethodOrPropertyName(thisAllowed::bool))]
token MethodOrPropertyName(thisAllowed::bool, out hasThis::bool)::LNode @{
result:ComplexNameDecl(thisAllowed, out hasThis)
| result:ComplexThisDecl(thisAllowed) {hasThis = true;}
| TT.Operator &{@["Expected 'true' or 'false'"] LT($LI).Value `is` bool} tf:TT.Literal
{ $result = F.Attr(_triviaUseOperatorKeyword, F.Literal(tf)); hasThis = false; }
};
// `this` with optional <type arguments>
token ComplexThisDecl(allowed::bool)::LNode @{
{if (!allowed) {Error("'this' is not allowed in this location.");};}
t:=TT.This {$result = F.Id(t);}
TParams(true, ref result)?
};
count::int; // hack allows Scan_TypeSuffixOpt() to compile
@[recognizer(fn Scan_TypeSuffixOpt(afterAsOrIs::bool))]
token TypeSuffixOpt(afterAsOrIs::bool, out dimensionBrack::opt!Token, ref e::LNode)::bool
@{
{count::int; result::bool = false;}
{dimensionBrack = null;}
greedy
( // in "x as Foo ? ..." it could be challenging to figure out whether
// "?" is part of type Foo or not. Some tests with the standard C#
// compiler seem to suggest that checking the next token will suffice,
// because standard C# rejects potentially valid code such as
// "x as Foo? - y", saying "Syntax error, ':' expected" at the end.
// This seems not to be a precedence issue; although "/" has higher
// precedence than "as", the C# compiler can parse "x as Foo? / y"
// (meaning (x as Foo?) / y) or "x as Foo + y" but fails to parse
// "x as Foo? + y". I believe this is because "+" can be a prefix
// operator. It also fails to parse "x as Foo?*" with a syntax error,
// not a semantic error, and for "x as Foo? * x" it again says
// "':' expected".
//
// Note: C# 7 introduces "x is Foo y" syntax, but "x is Foo? y" is
// illegal, just like "x as Foo? y" ("Syntax error, ':' expected")
//
// Examples (usually there is more than one syntax error, but the message
// "':' expected" indicates how the parser sees the situation):
// x as Foo ? y // Syntax error, ':' expected
// x as Foo? / y // Parsed OK
// x as Foo? % y // Parsed OK
// x as Foo? < y // Parsed OK
// x as Foo? ?? y // Parsed OK
// x as Foo ?? y // Parsed OK (?? is null coalesce operator)
// x as Foo? + y // Syntax error, ':' expected
// x as Foo? - y // Syntax error, ':' expected
// x as Foo? * -y // Syntax error, ':' expected
// x as Foo? ! y // Syntax error, ':' expected
// (x as Foo?++) // Invalid expression term ')'
// x as Foo?.Value // Parsed OK
// x as Foo?[y] // Parsed OK (whoa - looks like C# 6 introduced an ambiguity here with the `?[]` operator!)
// x as Foo ? (y) : z // Parsed OK
// x as Foo?(y) // Syntax error, ':' expected
t:="?" (&!{@[Local]afterAsOrIs} | &!(TT.Id | TT.ContextualKeyword | TT.TypeKeyword | TT.Literal
| TT.New | "(" | "{" | "+" | "-" | "*" | "&" | "!" | "~"
| "==>" | "++,--" | "$" | "@" | LinqKeywordAsId))
{e = F.Of(F.Id(t), e, e.Range.StartIndex, t.EndIndex); result=true;}
| // Standard C# fails to parse "x as Foo * y", which suggests I can
// unconditionally accept "*" as a pointer marker.
t:="*" {e = F.Of(F.Id(t), e, e.Range.StartIndex, t.EndIndex); result=true;}
| t:="**" {
var ptr = F.Id(S._Pointer, t.StartIndex, t.EndIndex);
e = F.Of(ptr, F.Of(ptr, e, e.Range.StartIndex, t.EndIndex - 1), e.Range.StartIndex, t.EndIndex);
result=true;
}
| // int[][,] means "array of (two-dimensional array of int)", so we
// must process array adornments in reverse order.
{var dims = InternalList!(Pair!(int,int)).Empty;}
{rb::Token;}
( &{@[Local] (count=CountDims(LT($LI), true)) > 0} lb:="[" rb="]" {dims.Add(Pair.Create(count, rb.EndIndex));})
(greedy(&{@[Local] (count=CountDims(LT($LI), false)) > 0} "[" rb="]" {dims.Add(Pair.Create(count, rb.EndIndex));}))*
{
if CountDims(lb, false) <= 0 {
dimensionBrack = lb;
};
for (i::int = dims.Count - 1; i >= 0; i--) {
e = F.Of(F.Id(S.GetArrayKeyword(dims[i].A)), e, e.Range.StartIndex, dims[i].B);
};
result = true;
}
)*
{return result;}
};
// ---------------------------------------------------------------------
// -- Expressions ------------------------------------------------------
// ---------------------------------------------------------------------
// Parsing EC# expressions is very tricky. Here are some of the things
// that make it difficult, especially in an LL(k) parser:
//
// 1. Parenthesis: is it a cast or a normal expression?
// (A<B,C>)y is a cast
// (A<B,C>)-y is NOT a cast
// (A<B,C>)(-y) is a cast
// (A<B,C>)(as B) is NOT a cast (well, the second part is)
// x(A<B,C>)(-y) is NOT a cast
// -(A<B,C>)(-y) is a cast
// $(A<B,C>)(-y) is NOT a cast
// (A<B,C>){y} is a cast
// (A<B,C>)[y] is NOT a cast
// (A<B,C>)++(y) is NOT a cast (it's post-increment and call)
// (A<B> C)(-y) is NOT a cast
// ([] A<B,C>)(y) is NOT a cast (empty attr list defeats cast)
// (int)*y is a cast
// (A<B,C>)*y is NOT a cast
// (A<B,C>)&y is NOT a cast
// (A+B==C)y is nonsensical
// x(A<B,C>)y is nonsensical
// 2. In-expr var declarations: is "(A<B,C>D)" a variable declaration?
// (A<B,C>D) => D.Foo() // variable declaration
// (A<B,C>D) = Foo() // variable declaration
// (f1, f2) = (A<B,C>D) // tuple
// Foo(A<B,C>D) // method with two arguments
// void Foo(A<B,C>D) // variable declaration at statement level
// Foo(A<B,C>D) // variable declaration at statement level if 'Foo' is the space name
// // (i.e. when 'Foo' is a constructor)
// 3. Less-than: is it a generics list or an operator? Need unlimited lookahead.
// (A<B,C) // less-than operator
// (A<B,C>) // generics list
// (A<B,C>D) // context-dependent
// 4. Brackets. Is "Foo[]" a data type, or an indexer with no args?
// (Foo[]) x; // data type: #of(@`'[]`, Foo)
// (Foo[]).x; // indexer: @`'[]`(Foo)
// 5. Does "?" make a nullable type or a conditional operator?
// Foo<B> ? x = null; // nullable type
// Foo<B> ? x = null : y; // conditional operator
/// Below lowest precedence
@[#public, #static, #readonly] StartExpr::Precedence = (new Precedence(-100));
// Types of expressions:
// - identifier
// - (parenthesized expr)
// - (tuple,)
// - ++prefix_operators
// - suffix_operators++
// - infix + operators, including x => y
// - the ? conditional : operator
// - generic<arguments>, generic!arguments, generic.[arguments]
// - (old_style) casts
// - call_style(->casts)
// - method_calls(with, arguments)
// - typeof(and other pseudofunctions)
// - indexers[with, indexes]
// - new Object()
// - { code in braces; new scope }
// - #{ code in braces; old scope }
// - delegate(...) {...}
// - from x in expr... (LINQ)
// Atom is: Id, TypeKeyword, $Atom, .Atom, new ..., (ExprStart), {Stmts},
@[LL(3)]
rule Atom::LNode @{
{r::LNode;}
( t:=("$"|".") e:=Atom {e=AutoRemoveParens(e);}
{r = F.Call(t.Value -> Symbol, e, t.StartIndex, e.Range.EndIndex, t.StartIndex, t.EndIndex);}
| op:=TT.Operator t:=AnyOperator
{r = F.Attr(_triviaUseOperatorKeyword, F.Id(t.Value -> Symbol, op.StartIndex, t.EndIndex));}
| t:=(TT.Id | TT.ContextualKeyword | TT.TypeKeyword | TT.This | TT.Base)
{r = F.Id(t);}
| t:=LinqKeywordAsId
{r = F.Id(t);}
| t:=TT.Literal
{r = F.Literal(t);}
| r=ExprInParensAuto // (...)
| r=NewExpr // new ...
| r=BracedBlock // {...}
| r=TokenLiteral
| t:=(TT.Checked | TT.Unchecked)
args:="(" rp:=")"
{r = F.Call(t.Value -> Symbol, ExprListInside(args), t.StartIndex, rp.EndIndex, t.StartIndex, t.EndIndex);}
| t:=(TT.Typeof | TT.Default | TT.Sizeof)
args:="(" rp:=")"
{r = F.Call(t.Value -> Symbol, TypeInside(args), t.StartIndex, rp.EndIndex, t.StartIndex, t.EndIndex);}
| t:=TT.Delegate args:="(" ")" block:="{" rb:="}"
{r = F.Call(S.Lambda, F.List(ExprListInside(args, false, true)), F.Braces(StmtListInside(block), block.StartIndex, rb.EndIndex),
t.StartIndex, rb.EndIndex, t.StartIndex, t.EndIndex, NodeStyle.OldStyle);}
| t:=TT.Is r=IsPattern(F.Missing, t)
| error
greedy[ (~(","|";")) ]*
{r = Error("'{0}': Expected an expression: (parentheses), {{braces}}, identifier, literal, or $substitution.", CurrentTokenText());}
/*hack: avoid long analysis-time bug*/ (_ =>{})
) {return r;}
};
token AnyOperator::Token // operators allowed after the "operator" keyword
@{
// Create synthetic token for >> or <<
&{LT($LI).EndIndex == LT($LI+1).StartIndex}
( op:="<" "<" {$result = (new Token(TT.Operator -> int, op.StartIndex, op.Length+1, S.Shl));}
| op:=">" ">" {$result = (new Token(TT.Operator -> int, op.StartIndex, op.Length+1, S.Shr));} )
/ result:
( "." | "->" | "::" | "?." | "=" | "??=" | ":" | "@"
| "``" | "\\" | "*" | "/,%" | "**" | "+" | "-" | "++,--"
| "&&" | "||,^^" | "!" | "&" | "^" | "|" | "~" | "==,!="
| "<" | ">" | "<=,>=" | ".." | "?" | "??" | "=:" | ":="
| "$" | "=>" | "==>" )
};
token NewExpr::LNode @{
{
majorDimension::opt!Token = null;
endIndex::int;
var list = VList!LNode.Empty;
}
op:=TT.New
( // new [] { ... } <=> #new(@`[]`(), ...)
&{(count=CountDims(LT($LI), false)) > 0}
lb:="[" rb:="]"
{var type = F.Id(S.GetArrayKeyword(count), lb.StartIndex, rb.EndIndex);}
lb="{" rb="}"
{
list.Add(LNode.Call(type, type.Range));
AppendInitializersInside(lb, ref list);
endIndex = rb.EndIndex;
}
| // new { ... } <=> #new(@``, ...)
lb:="{" rb:="}"
{
list.Add(F.Missing);
AppendInitializersInside(lb, ref list);
endIndex = rb.EndIndex;
}
| // new Type(...) <=> #new(Type(...))
// new Type(...) { ... } <=> #new(Type(...), ...)
// new Type { ... } <=> #new(Type(), ...)
// new Type[10] { ... } <=> #new(#of(@`[]`, Type)(10), ...)
// new Type[10][] <=> #new(#of(@`[]`, #of(@`'[]`, Type))(10))
type:=DataType(false, out majorDimension)
( // new Type(...) <=> #new(Type(...))
// new Type(...) { ... } <=> #new(Type(...), ...)
lp:="(" rp:=")"
{
if (majorDimension != null) {
Error("Syntax error: unexpected constructor argument list (...)");
};
list.Add(F.Call(type, ExprListInside(lp), type.Range.StartIndex, rp.EndIndex));
endIndex = rp.EndIndex;
}
( lb:="{" rb:="}"
{
AppendInitializersInside(lb, ref list);
endIndex = rb.EndIndex;
}
)?
/ // new Type { ... } <=> #new(Type(), ...)
// new Type[10] { ... } <=> #new(#of(@`[]`, Type)(10), ...)
// new Type[10][] <=> #new(#of(@`[]`, #of(@`[]`, Type))(10))
{var(lb::Token = op, rb::Token = op); haveBraces::bool = false;}
( lb="{" rb="}" {haveBraces = true;} )?
{
if (majorDimension != null) {
list.Add(LNode.Call(type, ExprListInside(majorDimension.Value), type.Range))
} else {
list.Add(LNode.Call(type, type.Range));
};
if (haveBraces) {
AppendInitializersInside(lb, ref list);
endIndex = rb.EndIndex;
} else {
endIndex = type.Range.EndIndex;
};
if (!haveBraces && majorDimension == null) {
if IsArrayType(type) {
Error("Syntax error: missing array size expression")
} else {
Error("Syntax error: expected constructor argument list (...) or initializers {...}");
};
};
}
)
)
{return F.Call(S.New, list, op.StartIndex, endIndex, op.StartIndex, op.EndIndex);}
};
fn TypeInside(args::Token)::LNode
{
if (!Down(args)) (return F.Id(S.Missing, args.EndIndex, args.EndIndex));
var type = DataType();
Match(EOF -> int);
return Up(type);
};
@[private] rule ExprInParensAuto::LNode @{
// This gate is used to avoid a slug that quadruples analysis time elsewhere in the grammar
"(" ")" =>
( &(ExprInParens(true) ("="|"=>"))
r:=ExprInParens(true) {return r;}
/ r:=ExprInParens(false) {return r;}
)
};
@[private] rule TokenLiteral()::LNode @{
at:"@" ( L:"[" R:"]" | L:"{" R:"}" )
{return F.Literal($L.Children, $at.StartIndex, $R.EndIndex);}
};
@[FullLLk, private]
rule AtomOrTypeParamExpr()::LNode @{
&( IdWithOptionalTypeParams(false) (~(TT.Id|TT.ContextualKeyword|TT.LinqKeyword)|EOF) ) result:IdWithOptionalTypeParams(false)
/ result:Atom
};
@[private] rule PrimaryExpr()::LNode @{
e:=AtomOrTypeParamExpr
FinishPrimaryExpr(ref e)
(
op:="?." rhs:=PrimaryExpr {e = F.Call(op, e, rhs, e.Range.StartIndex, rhs.Range.EndIndex, NodeStyle.Operator);}
)?
{return e;}
};
@[private] rule FinishPrimaryExpr(ref e::LNode) @{
greedy
( op:=("."|"->"|"::"|"=:")
rhs:=AtomOrTypeParamExpr {e = F.Call(op.Value -> Symbol, e, rhs, e.Range.StartIndex, rhs.Range.EndIndex, op.StartIndex, op.EndIndex, NodeStyle.Operator);}
/ // x(-> Foo), x(as Foo), x(using Foo)
e=PrimaryExpr_NewStyleCast(e)
/ lp:="(" rp:=")" {e = F.Call(e, ExprListInside(lp), e.Range.StartIndex, rp.EndIndex);}
| lb:="[" rb:="]" {var list = (new VList!LNode() { e });}
{e = F.Call(S.IndexBracks, AppendExprsInside(lb, list), e.Range.StartIndex, rb.EndIndex, lb.StartIndex, lb.EndIndex);}
| // Tentative tree structure - there is an undesirable inconsistency between ?. and ?[].
// Specifically, `a.b?.c.d` parses as `(a.b)?.(c.d)` so that the
// null-dot macro can manipulate the `.d` on the end; it would be
// harder to generate the outut of `a.b != null ? a.b.c.d : null` if
// it came in as `((a.b)?.c).d`. Likewise if there were a macro for
// `?[]`, it would morph `a.b?[c].d` into `a.b != null ? a.b[c].d : null`
// so it seems like we want a ternary operator, such that `a.b?[c].d`
// is parsed as @`?[]`(a.b, #(c), d) so we can deal with the `.d`.
// However, this doesn't really work, because it wouldn't naturally
// accept an input like `a.b?[c](d)`. So... argh. For now I'll parse
// 1. `a.b?[c].d` as ``@`?[]`(a.b, #(c)).d``
// 2. `a.b?[c](d)` as ``@`?[]`(a.b, #(c))(d)``
// This is the easiest way to parse it, anyhow. And I'll leave `?.`
// as-is, even though it works differently than `?[]`.
t:="?" lb:="[" rb:="]"
{e = F.Call(S.NullIndexBracks, e, F.List(ExprListInside(lb)), e.Range.StartIndex, rb.EndIndex, t.StartIndex, lb.EndIndex);}
| // Post-inc or post-dec
t:="++,--" {e = F.Call(t.Value == S.PreInc ? S.PostInc : S.PostDec, e, e.Range.StartIndex, t.EndIndex, t.StartIndex, t.EndIndex);}
| bb:=BracedBlockOrTokenLiteral
{ if (!e.IsCall || e.BaseStyle == NodeStyle.Operator) {
e = F.Call(e, bb, e.Range.StartIndex, bb.Range.EndIndex)
} else {
e = e.WithArgs(e.Args.Add(bb)).WithRange(e.Range.StartIndex, bb.Range.EndIndex);
};
}
)*
};
@[private] rule PrimaryExpr_NewStyleCast(e::LNode)::LNode @{
&{@[Hoist] Down($LI) && Up(LA0 == TT.As || LA0 == TT.Using || LA0 == TT.PtrArrow)}
lp:="(" rp:=")"
(=>
{Down(lp);}
{kind::Symbol;}
{var attrs = VList!LNode.Empty;}
( op:"->" {kind = S.Cast;}
| op:TT.As {kind = S.As;}
| op:TT.Using {kind = S.UsingCast;})
NormalAttributes(ref attrs)
AttributeKeywords(ref attrs)
type:=DataType EOF
{
type = type.PlusAttrs(attrs);
return Up(SetAlternateStyle(SetOperatorStyle(
F.Call(kind, e, type, e.Range.StartIndex, rp.EndIndex, op.StartIndex, op.EndIndex))));
}
)
};
// Prefix expressions, atoms, and high-precedence expressions like f(x) and List<T>
@[LL(3)] // to distinguish (cast) expr from (parens)
@[private] rule PrefixExpr::LNode
@{ op:=("+"|"-"|"!"|"~"|"++,--"|"*"|"&"|".."|"==>") e:=PrefixExpr
{return SetOperatorStyle(F.Call(op, e, op.StartIndex, e.Range.EndIndex));}
| op:="**"
e:=PrefixExpr
{return F.Call(S._Dereference, F.Call(S._Dereference, e,
op.StartIndex+1, e.Range.EndIndex, op.StartIndex+1, op.EndIndex, NodeStyle.Operator),
op.StartIndex, e.Range.EndIndex, op.StartIndex, op.StartIndex+1, NodeStyle.Operator);}
| // C-style cast
&{@[Hoist] Down($LI) && Up(Scan_DataType() && LA0 == EOF)}
lp:="(" ")"
&!("+"|"-"|"*"|"&"|"."|TT.BQString | ("++,--" "(") | &{_insideLinqExpr} TT.LinqKeyword)
e:=PrefixExpr
{Down(lp); return SetOperatorStyle(F.Call(S.Cast, e, Up(DataType()), lp.StartIndex, e.Range.EndIndex, lp.StartIndex, lp.EndIndex));}
/ e:=KeywordOrPrimaryExpr {return e;}
};
@[LL(2), private]
rule KeywordOrPrimaryExpr::LNode
@{ // C# 5 await
&{@[Hoist] Is($LI, @@await)} op:=TT.ContextualKeyword e:=PrefixExpr
{return SetOperatorStyle(F.Call(@@await, e, op.StartIndex, e.Range.EndIndex, op.StartIndex, op.EndIndex));}
/ e:=KeywordStmtAsExpr
{return e;}
/ e:=LinqQueryExpression
{return e;}
/ e:=PrimaryExpr
{return e;}
};
rule KeywordStmtAsExpr::LNode @{
{var startIndex = LT0.StartIndex;}
( result:ReturnBreakContinueThrow(startIndex)
| result:GotoCaseStmt(startIndex)
/ result:GotoStmt(startIndex)
| result:SwitchStmt(startIndex)
)
};
fn SetOperatorStyle(node::LNode)::LNode
{
return node.SetBaseStyle(NodeStyle.Operator);
};
fn SetAlternateStyle(node::LNode)::LNode
{
node.Style |= NodeStyle.Alternate;
return node;
};
// This rule handles all lower precedence levels, from ** down to assignment
// (=). This rule uses the "precedence floor" concept, documented in
// Loyc.Syntax.Precedence, to handle different precedence levels. The
// traditional approach is to define a separate rule for every precedence
// level. By collapsing many precedence levels into a single rule, there are
// two benefits:
// (1) shorter code.
// (2) potentially better performance: using a separate rule per precedence
// level causes a new stack frame and prediction step to be created for
// each level, regardless of the operators present in the actual
// expression being parsed. For example, to parse the simple expression
// "Hello" the traditional way requires 20 rules and therefore 20 stack
// frames if there are 20 precedence levels. On the other hand, the
// "precedence floor" approach requires an extra precedence check, so
// it may be slower when there are few levels. Note: EC# has 22
// precedence levels, and this method handles the lower 18 of them.
//
// The higher levels of the EC# expression parser do not use this trick
// because the higher precedence levels have some complications, such as
// C-style casts and <type arguments>, that I prefer to deal with
// separately.
@[private] rule SubExpr(context::Precedence)::LNode @{
{Debug.Assert(context.CanParse(EP.Prefix));}
{prec::Precedence;}
e:=PrefixExpr
greedy
( // Infix operator
&{@[Local] context.CanParse(prec=InfixPrecedenceOf($LA))}
op:=( "**"|"*"|"/,%"|"+"|"-"|"~"|".."|"<"|">"|"<=,>="|"==,!="
| "&"|"|"|"^"|"&&"|"||,^^"|"??"|"="|"??="|"=>"|TT.BQString|TT.In )
rhs:=SubExpr(prec)
{e = F.Call(op.Value -> Symbol, e, rhs, e.Range.StartIndex, rhs.Range.EndIndex, op.StartIndex, op.EndIndex, NodeStyle.Operator);}
| // is-test or a more complex is-expr
&{@[Local] context.CanParse(prec=EP.Compare)}
op:=TT.Is
e=IsPattern(e, op)
// 'as' or 'using' cast (special case: #usingCast instead of #using)
| &{@[Local] context.CanParse(prec=EP.Compare)}
op:=(TT.As | TT.Using)
rhs:=DataType(true)
{e = F.Call(op.Type() == TT.Using ? S.UsingCast : S.As, e, rhs, e.Range.StartIndex, rhs.Range.EndIndex, op.StartIndex, op.EndIndex);}
| // Shift operators (two LT or GT in a row)
&{@[Local] context.CanParse(EP.Shift)}
&{LT($LI).EndIndex == LT($LI+1).StartIndex}
( op:="<" "<" rhs:=SubExpr(EP.Shift)
{e = F.Call(S.Shl, e, rhs, e.Range.StartIndex, rhs.Range.EndIndex, op.StartIndex, op.EndIndex + 1, NodeStyle.Operator);}
| op:=">" ">" rhs:=SubExpr(EP.Shift)
{e = F.Call(S.Shr, e, rhs, e.Range.StartIndex, rhs.Range.EndIndex, op.StartIndex, op.EndIndex + 1, NodeStyle.Operator);}
)
| // Conditional operator
&{@[Local] context.CanParse(EP.IfElse)}
op:="?" then:=SubExpr(StartExpr) ":" else:=SubExpr(EP.IfElse)
{e = F.Call(S.QuestionMark, LNode.List(e, then, @else), e.Range.StartIndex, else.Range.EndIndex,
op.StartIndex, op.EndIndex, NodeStyle.Operator);}
)*
{return e;}
};
@[recognizer(fn Scan_IsPattern())]
@[private] token IsPattern(lhs::LNode, isTok::Token)::LNode // 'token' is used to avoid long analysis in LLLPG
@{
{argList::VList!LNode = (new VList!LNode(lhs));}
target:=DataType(true)
// C# 7 allows you to declare a variable as part of "is" expression
[targetName:=IdAtom {target = F.Call(S.Var, target, targetName, target.Range.StartIndex, targetName.Range.EndIndex);}]?
{argList.Add(target);}
// EC# allows an argument list for recursive deconstruction
[ "(" ")" {argList.Add(F.List(ExprListInside($"(", allowUnassignedVarDecl: true), $"(".StartIndex, $")".EndIndex));} ]?
{return F.Call(isTok, argList, lhs.Range.StartIndex, argList.Last.Range.EndIndex);}
};
// An expression that can start with attributes [...], attribute keywords
// (out, ref, public, etc.), a named argument (a: expr) and/or a variable
// declaration (Foo? x = null).
@[FullLLk, public] rule ExprStart(allowUnassignedVarDecl::bool)::LNode @{
// Detect "keyword argument" expression
{argName::Token = default(Token);}
(argName=(TT.Id|TT.ContextualKeyword)|argName=LinqKeywordAsId) colon:=":"
result:ExprStartNNP(allowUnassignedVarDecl)
{result = F.Call(S.NamedArg, F.Id(argName), result, argName.StartIndex, result.Range.EndIndex, colon.StartIndex, colon.EndIndex, NodeStyle.Operator);}
/ result:ExprStartNNP(allowUnassignedVarDecl)
};
// ExprStart with No Named Parameter allowed
@[public] rule ExprStartNNP(allowUnassignedVarDecl::bool)::LNode @{
{var attrs = VList!LNode.Empty;}
hasList:=NormalAttributes(ref attrs)
AttributeKeywords(ref attrs)
(SubExpr => {
// Unassigned var decl is allowed when attributes are present (e.g. out)
if (!attrs.IsEmpty || hasList) { allowUnassignedVarDecl = true; };
// PEG-style parse: try one thing, then the other.
expr::LNode;
var(result::TentativeResult, _::TentativeResult);
if (allowUnassignedVarDecl) {
expr = TentativeVarDecl(attrs, out result, allowUnassignedVarDecl)
?? TentativeExpr(attrs, out result);
} else {
expr = TentativeExpr(attrs, out result);
// If parsing as expression succeeds but it's an assignment with
// > on the RHS, try parsing as a var decl too (consider `List<Foo>x=0`)
if expr == null || (expr.Calls(S.Assign, 2) && expr.Args[0].Calls(S.GT, 2)) {
InputPosition = result.OldPosition;
expr = TentativeVarDecl(attrs, out _, allowUnassignedVarDecl);
};
};
expr ??= Apply(result);
return expr;
})
};
// Helper functions for parsing normal expressions and var-decl expressions
// TODO: upgrade to use newer LeMP macros that are easier to understand
unroll ((TentativeFunction; TentativeCode) `in` (
(TentativeExpr; #splice(
result.Result = SubExpr(StartExpr).PlusAttrs(attrs);
// A var decl like "A B" looks like an expression followed by
// an identifier. To cede victory to VarDeclExpr, detect that
// the expression didn't end properly and emit an error.
if LA0 != EOF && LA0 != TT.Semicolon && LA0 != TT.Comma && !(LA0 == TT.LinqKeyword && _insideLinqExpr) {
failed = @true;
};
));
(TentativeVarDecl; #splice(
_::int;
var cat = DetectStatementCategoryAndAddWordAttributes(out _, ref attrs, DetectionMode.Expr);
if (cat != StmtCat.MethodOrPropOrVar) {
failed = @true;
result.Result = F.Missing;
} else {
hasInitializer::bool;
result.Result = VarDeclExpr(out hasInitializer, attrs);
if (!hasInitializer && !allowUnassigned) {
Error(-1, "An unassigned variable declaration is not allowed in this context");
};
};
));
)) {
// Parses an expression (TentativeExpr) or variable declaration (TentativeVarDecl).
// Returns the parsed node on success or null if outer-level parse error(s)
// occurred; the out param result is never null, and in case of success it
// is the same as the return value. Error handling is tricky... we fail if
// there are errors at the current level, not if there are errors in
// parenthesized subexpressions.
fn TentativeFunction(attrs::VList!LNode, out result::TentativeResult, allowUnassigned::bool = false)::LNode
{
result = (new TentativeResult(InputPosition));
var oldState = _tentative;
_tentative = (new TentativeState(@true));
{
on_finally { _tentative = oldState; };
failed::bool = false;
TentativeCode; // sets the result variable
result.Errors = _tentative.DeferredErrors;
result.InputPosition = InputPosition;
if failed || _tentative.LocalErrorCount != 0 {
// error(s) occurred.
InputPosition = result.OldPosition;
return @null;
};
};
return Apply(result); // must Apply after finally block
};
};
fn Apply(result::TentativeResult)::LNode
{
InputPosition = result.InputPosition;
if result.Errors != null {
result.Errors.WriteListTo(CurrentSink(false));
};
return result.Result;
};
@[private] rule VarDeclExpr(out hasInitializer::bool, VList<LNode> attrs)::LNode @{
[ t:=TT.This {attrs.Add(F.Id(t));} ]?
pair:=VarDeclStart
{var(type::LNode = pair.Item1, name::LNode = pair.Item2);}
( // A property is allowed whereever an initialized variable is
// allowed, to make possible `Constructor(public int Prop {get;}) {}`
result:RestOfPropertyDefinition(type.Range.StartIndex, type, name, true)
{hasInitializer = true;}
/ nameAndInit:=VarInitializerOpt(name, IsArrayType(type))
{
hasInitializer = (nameAndInit != name);
var typeStart = type.Range.StartIndex;
var start = attrs.IsEmpty ? typeStart : attrs[0].Range.StartIndex;
$result = F.Call(S.Var, type, nameAndInit,
start, nameAndInit.Range.EndIndex, typeStart, typeStart);
hasInitializer = @true;
}
)
{$result = $result.PlusAttrs(attrs);}
};
@[private] rule VarDeclStart::Pair!(LNode, LNode) @{
e:=DataType
id:=IdAtom
{MaybeRecognizeVarAsKeyword(ref e);}
{return Pair.Create(e, id);}
};
@[#static, #readonly] _var::Symbol = GSymbol.Get("var");
@[private] fn MaybeRecognizeVarAsKeyword(ref type::LNode)
{
// Recognize "var" (but not @var) as a contextual keyword.
rng::SourceRange;
name::Symbol = type.Name;
if (name == _var) && type.IsId
&& (rng=type.Range).Source.Text.TryGet(rng.StartIndex, '\0') != '@' {
type = type.WithName(S.Missing);
};
};
@[private] rule ExprInParens(allowUnassignedVarDecl::bool)::LNode
@{
lp:="(" rp:=")"
{
// Note: we invoke ExprInParensOrTuple inside an action, hiding the
// invocation from LLLPG, to avoid creating a recognizer for Expr.
if (!Down(lp)) { return F.Call(S.Tuple, lp.StartIndex, rp.EndIndex, lp.StartIndex, lp.EndIndex); };
return Up(InParens_ExprOrTuple(allowUnassignedVarDecl, lp.StartIndex, rp.EndIndex));
}
};
// Called inside parens by ExprInParens
rule InParens_ExprOrTuple(allowUnassignedVarDecl::bool, startIndex::int, endIndex::int)::LNode @
{ // We're parsing a token tree so EOF could represent ")" or "]" or "}"
EOF =>
{return F.Tuple(VList!LNode.Empty, startIndex, endIndex);}
/ {var hasAttrList = LA0 == TT.LBrack;}
e:=ExprStart(allowUnassignedVarDecl)
{var list = (new VList!LNode() { e });}
nongreedy[
"," list+=ExprStart(allowUnassignedVarDecl)
]*
{isTuple::bool = list.Count > 1;}
["," {isTuple = true;}]? EOF
{
if isTuple {
return F.Tuple(list, startIndex, endIndex);
} else {
return hasAttrList ? e : F.InParens(e, startIndex, endIndex);
};
}
};
@[private] rule BracedBlockOrTokenLiteral(spaceName::Symbol = @null, target::Symbol = @null, startIndex::int = -1)::LNode
@{ result:BracedBlock(spaceName, target, startIndex)
| result:TokenLiteral
};
@[private] rule BracedBlock(spaceName::Symbol = @null, target::Symbol = @null, startIndex::int = -1)::LNode
@{
{ var oldSpace = _spaceName;
_spaceName = spaceName ?? oldSpace;
}
"{" "}"
{ if (startIndex == -1) { startIndex = $"{".StartIndex; };
var stmts = StmtListInside($"{");
_spaceName = oldSpace;
return F.Call(target ?? S.Braces, stmts,
startIndex, $"}".EndIndex, $"{".StartIndex, $"{".EndIndex, NodeStyle.Statement);
}
};
// ---------------------------------------------------------------------
// -- Attributes -------------------------------------------------------
// ---------------------------------------------------------------------
@[recognizer(fn Scan_NormalAttributes())]
token NormalAttributes(ref attrs::VList!LNode)::bool @{
( &!{Down($LI) && Up(Try_Scan_AsmOrModLabel(0))}
t:="[" "]"
{
$result = true;
if (Down(t)) {
AttributeContents(ref attrs);
Up();
};
}
)*
};
token AttributeContents(ref attrs::VList!LNode) @{
{attrTarget::Token = default(Token);}
( attrTarget=(TT.Id|TT.ContextualKeyword|TT.LinqKeyword|TT.Return) ":"
{newAttrs::VList!LNode = (new VList!LNode());}
ExprList(ref newAttrs, allowTrailingComma: true, allowUnassignedVarDecl: true)
{
var attrTargetId = F.Id(attrTarget);
for (i::int = 0; i < newAttrs.Count; i++) {
var attr = newAttrs[i];
if (!IsNamedArg(attr)) {
attr = SetOperatorStyle(F.Call(S.NamedArg, attrTargetId, attr,
i == 0 ? attrTarget.StartIndex : attr.Range.StartIndex, attr.Range.EndIndex));
} else {
Error(attrTargetId = attrs[i].Args[0], "Syntax error: only one attribute target is allowed");
};
attrs.Add(attr);
};
}
/ ExprList(ref attrs, allowTrailingComma: true, allowUnassignedVarDecl: true)
)
};
fn IsNamedArg(node::LNode)::bool { return node.Calls(S.NamedArg, 2) && node.BaseStyle == NodeStyle.Operator; };
token AttributeKeywords(ref attrs::VList!LNode) @{
[ t:=TT.AttrKeyword
{attrs.Add(F.Id(t));}
]*
};
@[recognizer(fn Scan_TParamAttributeKeywords())]
token TParamAttributeKeywords(ref attrs::VList!LNode) @{
[ t:=(TT.AttrKeyword|TT.In)
{attrs.Add(F.Id(t));}
]*
};
// =====================================================================
// == LINQ =============================================================
// =====================================================================
@[public] rule LinqQueryExpression::LNode @{
{
startIndex::int = LT0.StartIndex;
_insideLinqExpr = true;
on_finally { _insideLinqExpr = false; };
var parts = LNode.List();
}
parts+=LinqFromClause
QueryBody(ref parts)
{return F.Call(S.Linq, parts, startIndex, parts.Last.Range.EndIndex, startIndex, startIndex);}
};
@[private] rule LinqFromClause::LNode @{
&{@[Hoist] Is($LI, @@from)} kw:=TT.LinqKeyword
((TT.Id|TT.ContextualKeyword|"$") =>)
e:=Var_In_Expr
{return F.Call(S.From, e, kw.StartIndex, e.Range.EndIndex, kw.StartIndex, kw.EndIndex);}
};
rule Var_In_Expr::LNode
@{ &VarIn
VarIn(out inTok::Token)
e:=ExprStart(false)
{return F.Call(S.In, $VarIn, e, $VarIn.Range.StartIndex, e.Range.EndIndex, inTok.StartIndex, inTok.EndIndex);}
/ result:ExprStart(false)
};
@[LL(1)]
@[private] rule QueryBody(ref parts::VList!LNode) @{
greedy[parts+=QueryBodyClause]*
( parts+=LinqGroupClause
| parts+=LinqSelectClause
| error {Error("Expected 'select' or 'group' clause to end LINQ query");}
)
[parts+=QueryContinuation]?
};
@[LL(1)]
@[private] rule QueryBodyClause::LNode
@{ result:LinqFromClause
| result:LinqLet
| result:LinqWhere
| result:LinqJoin
| result:LinqOrderBy
};
@[private] rule LinqLet::LNode @{
&{@[Hoist] Is($LI, @@let)} kw:=TT.LinqKeyword e:=ExprStart(false)
{if (!e.Calls(S.Assign, 2)) { Error("Expected an assignment after 'let'"); };}
{return F.Call(S.Let, e, kw.StartIndex, e.Range.EndIndex, kw.StartIndex, kw.EndIndex);}
};
@[private] rule LinqWhere::LNode @{
&{@[Hoist] Is($LI, @@where)} kw:=TT.LinqKeyword e:=ExprStart(false)
{return F.Call(S.Where, e, kw.StartIndex, e.Range.EndIndex, kw.StartIndex, kw.EndIndex);}
};
@[LL(1)]
@[private] rule LinqJoin::LNode @{
&{@[Hoist] Is($LI, @@join)} kw:=TT.LinqKeyword from:Var_In_Expr
&{@[Hoist] Is($LI, @@on)} TT.LinqKeyword
lhs:=ExprStart(false)
&{@[Hoist] Is($LI, @@equals)} eq:=TT.LinqKeyword
rhs:=ExprStart(false)
{var equality = F.Call(@@#equals, lhs, rhs, lhs.Range.StartIndex, rhs.Range.EndIndex, eq.StartIndex, eq.EndIndex);}
( // "into" marks a "group join". For example, the query
// from c in categories
// join p in products on c.ID equals p.CategoryID into productGroup
// select new { Name = c.Name, Products = productGroup }
// means "create a sequence of product groups, each containing a list of products with the same CategoryID"
&{Is($LI, @@into)} intoKw:=TT.LinqKeyword IdAtom
{var into = F.Call(S.Into, $IdAtom, intoKw.StartIndex, $IdAtom.Range.EndIndex, intoKw.StartIndex, intoKw.EndIndex);}
{var args = LNode.List(from, equality, into);}
{return F.Call(S.Join, args, kw.StartIndex, into.Range.EndIndex, kw.StartIndex, kw.EndIndex);}
/ // No suffix: normal inner join
{return F.Call(S.Join, from, equality, kw.StartIndex, equality.Range.EndIndex, kw.StartIndex, kw.EndIndex);}
)
};
@[LL(1)]
@[private] rule LinqOrderBy::LNode @{
&{@[Hoist] Is($LI, @@orderby)} kw:=TT.LinqKeyword
{var parts = LNode.List();}
parts+=LinqOrdering ["," parts+=LinqOrdering]*
{return F.Call(S.OrderBy, parts, kw.StartIndex, parts.Last.Range.EndIndex, kw.StartIndex, kw.EndIndex);}
};
@[LL(1)]
@[private] rule LinqOrdering::LNode @{
result:ExprStart(false)
greedy
[ &{Is($LI, @@ascending)} dir:TT.LinqKeyword
{$result = F.Call(S.Ascending, result, result.Range.StartIndex, dir.EndIndex, dir.StartIndex, dir.EndIndex);}
| &{Is($LI, @@descending)} dir:TT.LinqKeyword
{$result = F.Call(S.Descending, result, result.Range.StartIndex, dir.EndIndex, dir.StartIndex, dir.EndIndex);}
]?
};
@[private] rule LinqSelectClause::LNode @{
&{@[Hoist] Is($LI, @@select)} kw:=TT.LinqKeyword e:=ExprStart(false)
{return F.Call(S.Select, e, kw.StartIndex, e.Range.EndIndex, kw.StartIndex, kw.EndIndex);}
};
@[private] rule LinqGroupClause::LNode @{
&{@[Hoist] Is($LI, @@group)} kw:TT.LinqKeyword lhs:ExprStart(false)
( &{@[Hoist] Is($LI, @@by) } by:TT.LinqKeyword rhs:ExprStart(false)
| error {Error("Expected 'by'"); rhs = MissingHere();} )
{return F.Call(S.GroupBy, lhs, rhs, kw.StartIndex, rhs.Range.EndIndex, kw.StartIndex, kw.EndIndex);}
};
@[LL(1)]
@[private] rule QueryContinuation::LNode @{
&{@[Hoist] Is($LI, @@into)} kw:TT.LinqKeyword IdAtom
{var parts = LNode.List($IdAtom);}
QueryBody(ref parts)
{return F.Call(S.Into, parts, kw.StartIndex, parts.Last.Range.EndIndex, kw.StartIndex, kw.EndIndex);}
};
// =====================================================================
// == Statements =======================================================
// =====================================================================
_stmtAttrs::WList!LNode = (new WList!LNode());
@[LL(3)]
@[public] rule Stmt::LNode @{
{var attrs = VList!LNode.Empty;}
{startIndex::int = LT0.StartIndex;}
NormalAttributes(ref attrs)
AttributeKeywords(ref attrs)
(=>
{ // Analysis is tough for LLLPG here, especially the question "is
// this a var/prop/method or something else?", but also questions
// like "is this a `this` constructor, `this[...]` prop, or just
// an expression that starts with `this`?", so I've written custom
// prediction analysis (DetectStatementCategory), which doubles as
// a detector for "word attributes" like "partial".
wordAttrCount::int;
var cat = DetectStatementCategoryAndAddWordAttributes(out wordAttrCount, ref attrs, DetectionMode.Stmt);
switch (cat) {
case StmtCat.MethodOrPropOrVar;
$result = MethodOrPropertyOrVarStmt(startIndex, attrs);
break;
case StmtCat.KeywordStmt;
$result = KeywordStmt(startIndex, attrs, wordAttrCount != 0);
break;
case StmtCat.IdStmt;
$result = IdStmt(startIndex, attrs, wordAttrCount != 0);
break;
case StmtCat.OtherStmt;
$result = OtherStmt(startIndex, attrs, wordAttrCount != 0);
break;
case StmtCat.ThisConstructor;
$result = Constructor(startIndex, attrs);
break;
default;
throw (new Exception("Parser bug"));
};
})
};
// Statement categories distinguished by DetectStatementCategory
enum StmtCat {
MethodOrPropOrVar = 0; // method, property, or variable declaration
KeywordStmt = 1; // declarative like struct/enum/event, or executable like while/for/try
IdStmt = 2; // a statement that starts with an Id and doesn't allow keyword attributes;
// in certain cases it's also used for stmts that start with "$" or a type keyword
ThisConstructor = 3; // a constructor named 'this'
OtherStmt = 4; // everything else (word attributes not allowed)
};
// Tweak detection behavior depending on context
enum DetectionMode {
Expr = 0;
Stmt = 1;
};
@[static, readonly] ExpectedAfterTypeAndName_InStmt::HashSet!TT = (new HashSet!TT {
TT.Set; TT.LBrace; TT.LParen; TT.LBrack;
TT.LambdaArrow; TT.Forward; TT.Semicolon; TT.Comma;
TT.At; // @{...} token literal (property)
});
@[static, readonly] ExpectedAfterTypeAndName_InExpr::HashSet!TT = (new HashSet!TT {
TT.Set; TT.LBrace;
TT.LambdaArrow; TT.Forward; TT.Semicolon; TT.Comma; TT.EOF;
TT.At; // @{...} token literal (property)
});
@[static, readonly] ExpectedAfterTypeAndName::array!(HashSet!TT) = (new array!(HashSet!TT) {
ExpectedAfterTypeAndName_InExpr;
ExpectedAfterTypeAndName_InStmt;
});
@[static, readonly] EasilyDetectedKeywordStatements::HashSet!TT = (new HashSet!TT {
TT.Namespace; TT.Class; TT.Struct;
TT.Interface; TT.Enum ; TT.Event;
TT.Switch ; TT.Case ; TT.Using;
TT.While ; TT.Fixed; TT.For;
TT.Foreach ; TT.Goto ; TT.Lock;
TT.Return ; TT.Try ; TT.Do;
TT.Continue ; TT.Break; TT.Throw;
TT.If;
});
fn DetectStatementCategoryAndAddWordAttributes(out wordAttrCount::int, ref attrs::VList!LNode, mode::DetectionMode)::StmtCat
{
// (This is also called if a variable declaration is suspected in an
// expression. In this case, detecting the statement category isnt
// the main goal; rather, the main goal is figuring out where the
// word attributes end.)
var oldPosition = InputPosition;
var cat = DetectStatementCategory(out wordAttrCount, mode);
// Add word attributes, if any
for (; oldPosition < InputPosition; oldPosition++) {
word::Token = _tokens[oldPosition];
wordAttr::LNode;
if (word.Kind == TokenKind.AttrKeyword || word.Type() == TT.New) {
wordAttr = F.Id(word);
} else {
wordAttr = F.Attr(_triviaWordAttribute, F.Id("#" + word.Value.ToString(), word.StartIndex, word.EndIndex));
};
attrs.Add(wordAttr);
};
return cat;
};
// Originally I let LLLPG distinguish the statement types, but when the
// parser was mature, LLLPG's analysis took from 10 to 40 seconds and it
// generated 5000 lines of code to distinguish the various cases.
// This custom code, which was very tricky to get right,
// (1) detects method/property/variable (and lookalikes: trait, alias) and
// distinguishes certain other cases (StmtCat values) out of convenience.
// (2) detects and skips word attributes (not including `this` in `this int x`).
// (3) is fast if possible.
//
// "word attributes" are a messy feature of EC#. C# has a few non-keyword
// attributes that appear at the beginning of a statement, such as "partial",
// "yield" and "async". EC# generalizes this concept to "word attributes", which
// can be ANY normal identifier.
//
// BTW: "word attributes" are not really "contextual keywords" in the C#
// sense, because the identifiers are not recognized by the parser. The
// only true contextual keywords in EC# are "var", "module", "assembly",
// "from" (LINQ), "trait", "alias", and "when"; others like "partial" and
// "yield" are merely "word attributes".
//
// Since they're not keywords, word attributes are difficult because:
// 1. Lots of statements start with words that are not word attributes,
// such as "foo=0", "foo* x = null", "foo x { ... }", and "foo x();"
// Somehow we have to figure out which words are attributes.
// 2. Not all statements accept word attributes. But the attributes
// appear BEFORE the statement, so how can we tell if we should
// be expecting word attributes or not?
//
// There are lots of "keyword-based" statements that accept word
// attributes (think "partial class" and "yield return"), which are the
// easiest to handle. Our main difficulty is the non-keyword statements:
// 1. statements that can begin with an identifier, but also accept
// word attributes:
// - word type* name ... // field, property or method
// - word type? name ... // field, property or method
// - word type[,][] name ... // field, property or method
// - word type<x> name ... // field, property or method
// - word type name(); // method decl
// - word type name<x>(); // method decl
// - word type<x> name(); // method decl
// - word type<x> name<x>(); // method decl
// - word this(); // except inside methods
// - word this<x>(); // except inside methods
// - word label:
// 2. statements that can start with an identifier, and do not accept
// word attributes:
// - foo(); // method call OR constructor
// - foo<x>();
// - foo = 0;
// - foo - bar;
//
// Notice that if a word is followed by an operator of any kind, it
// can't be an attribute; but if it is followed by another word, it's
// unclear. In particular, notice that for "A B<x>...", "A" is sometimes
// an attribute and sometimes not. In "A B<x> C", A is an attribute,
// but in "A B<x>()" it is a return type. The same difficulty exists
// in case of alternate generics specifiers such as "A B!(x)" and types
// with suffixes, like "A int?*".
fn DetectStatementCategory(out wordAttrCount::int, mode::DetectionMode)::StmtCat
{
// If the statement starts with identifier(s), we have to figure out
// whether, and how many of them, are word attributes. Also, skip over
// `new` because can be a keyword attribute in some cases.
wordAttrCount = 0;
wordsStartAt::int = InputPosition;
haveNew::bool = LA0 == TT.New; // "new" keyword is the most annoying wrinkle
if (haveNew || LA0 == TT.Id || LA0 == TT.ContextualKeyword || LA0 == TT.LinqKeyword)
{
if (!haveNew) {
// Optimized path for common expressions that start with an Id (IdStmts)
// first lets get it working without optimization
/*var la1k = LT(1).Kind;
if la1k == TokenKind.LParen || la1k == TokenKind.Assignment ||
la1k == TokenKind.Separator || la1k->int == TT.EOF->int {
return StmtCat.IdStmt;
} else if (la1k == TokenKind.Operator) {
var la1 = LA(1);
if la1 != TT.QuestionMark && la1 != TT.LT && la1 != TT.Mul {
return StmtCat.IdStmt;
};
};*/
};
// Scan past identifiers and extra AttrKeywords at beginning of statement
isAttrKw::bool = haveNew;
do {
Skip();
if (isAttrKw) {
wordsStartAt = InputPosition;
} else {
wordAttrCount++;
};
haveNew |= (isAttrKw = (LA0 == TT.New));
} while ((isAttrKw |= LA0 == TT.AttrKeyword || LA0 == TT.New)
|| LA0 == TT.Id || LA0 == TT.ContextualKeyword || LA0 == TT.LinqKeyword);
};
// At this point we've skipped over all simple identifiers.
// Now decide: what kind of statement do we appear to have?
consecutive::int = InputPosition - wordsStartAt;
if (LA0 == TT.TypeKeyword) {
// We can treat this as if it were one additional identifier,
// although it cannot be treated as a word attribute.
InputPosition++;
consecutive++;
} else if (LA0 == TT.Substitute) {
// We can treat this as if it were one additional identifier,
// although it cannot be treated as a word attribute.
var la1 = LA(1);
if (LA(1) == TT.LParen && LA(2) == TT.RParen) {
InputPosition += 3;
} else {
InputPosition++;
};
consecutive++;
} else if (LA0 == TT.This) {
if LA(1) == TT.LParen && LA(2) == TT.RParen {
la3::TT = LA(3);
if la3 == TT.Colon || la3 == TT.LBrace || la3 == TT.Semicolon && _spaceName != S.Fn {
return StmtCat.ThisConstructor;
} else {
return StmtCat.OtherStmt;
};
} else if (consecutive != 0) {
// Appears to be a this[] property (there could be a type
// param, like this<T>[], so we can't check if LA(1) is "[")
InputPosition--;
return StmtCat.MethodOrPropOrVar;
}
} else if (LT0.Kind == TokenKind.OtherKeyword) {
if (EasilyDetectedKeywordStatements.Contains(LA0)
|| LA0 == TT.Delegate && LA(1) != TT.LParen
|| (LA0 == TT.Checked || LA0 == TT.Unchecked) && LA(1) == TT.LBrace)
{
// `if` and `using` do not support word attributes:
// - `if`, because in the original plan EC# was to support a
// D-style 'if' clause at the end of property definitions,
// making "T P if ..." ambiguous if word attributes were allowed.
// - `using` because `x using T` was planned as a new cast operator.
unless (consecutive > 0 && (LA0 == TT.If || LA0 == TT.Using)) {
return StmtCat.KeywordStmt;
};
};
} else if (consecutive == 0) {
if (!haveNew) {
return StmtCat.OtherStmt;
};
};
// At this point we know it's not a "keyword statement" or "this constructor",
// so it's either MethodOrPropOrVar, which allows word attributes, or
// something else that prohibits them (IdStmt or OtherStmt).
if consecutive >= 2 {
// We know it's MethodOrPropOrVar, but where do the word attributes end?
likelyStart::int = wordsStartAt + consecutive - 2; // most likely location
if (ExpectedAfterTypeAndName[mode->int].Contains(LA0)) {
InputPosition = likelyStart;
} else {
// We must distinguish among these three cases:
// 1. IEnumerator IEnumerable.GetEnumerator()
// ^likelyStart(correct) ^InputPosition
// 2. alias Map<K,V> = Dictionary <K,V>;
// ^likelyStart(correct) ^InputPosition
// 3. partial Namespace.Class Method()
// ^likelyStart(too low) ^InputPosition
InputPosition = likelyStart + 1;
if (Scan_ComplexNameDecl() && ExpectedAfterTypeAndName[mode->int].Contains(LA0)) {
InputPosition = likelyStart;
} else {
InputPosition = likelyStart + 1;
};
};
return StmtCat.MethodOrPropOrVar;
};
// Worst case: need arbitrary lookahead to detect var/property/method
InputPosition = wordsStartAt;
using (new SavePosition(this, 0)) {
TryMatch(TT.This->int); // skip 'this' attribute for extension methods
// Use MethodOrPropertyName to detect all possible method, property,
// and variable declarations (this incidentally matches some invalid
// things like `bool operator true { get {} }`.)
if (Scan_DataType(false) && Scan_MethodOrPropertyName(true) && ExpectedAfterTypeAndName[mode->int].Contains(LA0)) {
return StmtCat.MethodOrPropOrVar;
};
};
if (haveNew) {
if (LA(-1) == TT.New) {
InputPosition--;
} else {
// count 'new' as a word attribute, to trigger an error if it shouldn't be there
wordAttrCount++;
}
};
return consecutive != 0 ? StmtCat.IdStmt : StmtCat.OtherStmt;
};
// Methods, properties, variables, and things that look like them (trait & alias)
rule MethodOrPropertyOrVarStmt(startIndex::int, attrs::VList!LNode)::LNode @{
( result:TraitDecl(startIndex) {result = result.PlusAttrs(attrs);}
/ result:AliasDecl(startIndex) {result = result.PlusAttrs(attrs);}
/ result:MethodOrPropertyOrVar(startIndex, attrs)
)
};
// Statements that begin with a keyword
rule KeywordStmt(startIndex::int, attrs::VList!LNode, hasWordAttrs::bool)::LNode @{
{
r::LNode;
addAttrs::bool = true; // causes attrs to be added to result
showWordAttrErrorFor::string = null; // causes error when hasWordAttrs
}
( r=IfStmt(startIndex)
{showWordAttrErrorFor = "if statement"; addAttrs = true;}
| r=EventDecl(startIndex)
| r=DelegateDecl(startIndex, attrs) {addAttrs = false;}
| r=SpaceDecl(startIndex) // namespace, class, struct, interface
| r=EnumDecl(startIndex)
| r=CheckedOrUncheckedStmt(startIndex)
| r=DoStmt(startIndex)
| r=CaseStmt(startIndex)
| // Note: although the statements `return`, `throw`, `break`, `continue`,
// `goto`, `goto case` and `switch` can be parsed as expressions,
// contextual keywords like `yield return` only work in a statement
// context. So it was easier to leave this here and not change the code
// that handles contexual keywords. (in addition, `switch` must be
// handled here so we can suppress the need for a final semicolon)
r=ReturnBreakContinueThrow(startIndex) ";"
| r=GotoCaseStmt(startIndex) ";"
/ r=GotoStmt(startIndex) ";"
| // `switch` can also be parsed as an expression. It
r=SwitchStmt(startIndex)
| r=WhileStmt(startIndex)
| r=ForStmt(startIndex)
| r=ForEachStmt(startIndex)
| r=UsingStmt(startIndex)
{showWordAttrErrorFor = "using statement";}
/ r=UsingDirective(startIndex, attrs) {addAttrs = false;}
{showWordAttrErrorFor = "using directive";}
| r=LockStmt(startIndex)
| r=FixedStmt(startIndex)
| r=TryStmt(startIndex)
| error {r = Error("Bug: Keyword statement expected, but got '{0}'", CurrentTokenText());}
ScanToEndOfStmt
)
{
if addAttrs {
r = r.PlusAttrs(attrs);
};
if hasWordAttrs && showWordAttrErrorFor != null {
NonKeywordAttrError(attrs, showWordAttrErrorFor);
};
return r;
}
};
// Statements that start with an Id and don't allow keyword attributes
// This may also be called for expression-statements that start with "$" or a type keyword
rule IdStmt(startIndex::int, attrs::VList!LNode, hasWordAttrs::bool)::LNode @{
{
addAttrs::bool = false; // causes attrs to be added to result
showWordAttrErrorFor::string = null; // causes error when hasWordAttrs
}
( result:Constructor(startIndex, attrs)
{showWordAttrErrorFor = "old-style constructor";}
/ result:BlockCallStmt(startIndex)
{showWordAttrErrorFor = "block-call statement"; addAttrs = true;}
/ result:LabelStmt(startIndex)
{addAttrs = true;}
/ &(DataType TT.This) // property this[]
DataType => result:MethodOrPropertyOrVar(startIndex, attrs)
/ result:ExprStatement()
{showWordAttrErrorFor = "expression"; addAttrs = true;}
)
{
if addAttrs {
result = result.PlusAttrs(attrs);
};
if hasWordAttrs && showWordAttrErrorFor != null {
NonKeywordAttrError(attrs, showWordAttrErrorFor);
};
}
};
fn NonKeywordAttrError(attrs::IList!LNode, stmtType::string)
{
var attr = attrs.FirstOrDefault(a => a.AttrNamed(S.TriviaWordAttribute) != null);
if (attr != null) {
Error(attr, "'{0}' appears to be a word attribute, which is not permitted before '{1}'", attr.Range.SourceText, stmtType);
};
};
// Statements that don't start with an Id and don't allow keyword attributes.
@[FullLLk]
rule OtherStmt(startIndex::int, attrs::VList!LNode, hasWordAttrs::bool)::LNode @{
{
addAttrs::bool = false; // causes attrs to be added to result
showWordAttrErrorFor::string = null; // causes error when hasWordAttrs
}
( result:BracedBlock(null, null, startIndex)
{showWordAttrErrorFor = "braced-block statement"; addAttrs = true;}
/ &("~" (TT.Id|TT.ContextualKeyword|TT.LinqKeyword|TT.This) "(" ")" "{" "}")
result:Destructor(startIndex, attrs)
{showWordAttrErrorFor = "destructor";}
/ ";" {$result = F.Id(S.Missing, startIndex, $";".EndIndex);}
{showWordAttrErrorFor = "empty statement"; addAttrs = true;}
/ result:LabelStmt(startIndex) // includes default:
{addAttrs = true;}
/ default result:ExprStatement()
{showWordAttrErrorFor = "expression"; addAttrs = true;}
/ result:AssemblyOrModuleAttribute(startIndex, attrs)
{showWordAttrErrorFor = "assembly or module attribute";}
/ result:OperatorCastMethod(startIndex, attrs) {attrs.Clear();}
// / ths:TT.This
// &(DataType ComplexNameDecl)
// {attrs.Add(F.Id(ths));}
// result:MethodOrPropertyOrVar(startIndex, attrs)
/ error
{$result = Error("Statement expected, but got '{0}'", CurrentTokenText());}
ScanToEndOfStmt
)
{
if addAttrs {
result = result.PlusAttrs(attrs);
};
if hasWordAttrs && showWordAttrErrorFor != null {
NonKeywordAttrError(attrs, showWordAttrErrorFor);
};
}
};
rule ExprStatement::LNode @{
result:SubExpr(StartExpr)
( (EOF | TT.Else | TT.While | TT.Catch | TT.Finally) =>
{var rr = result.Range;}
{result = F.Call(S.Result, result, rr.StartIndex, rr.EndIndex, rr.StartIndex, rr.StartIndex);}
| ";" {result = result.WithRange(result.Range.StartIndex, $";".EndIndex);}
| error {result = Error("Syntax error in expression at '{0}'; possibly missing semicolon", CurrentTokenText());}
ScanToEndOfStmt
)
};
@[private] token ScanToEndOfStmt() @{
// Used for error recovery
(greedy(~(";"|"{")))*
greedy(";" | "{" "}"?)?
};
// ---------------------------------------------------------------------
// namespace, class, struct, interface, trait, alias, using, enum ------
// ---------------------------------------------------------------------
@[private] rule SpaceDecl(startIndex::int)::LNode @{
t:=(TT.Namespace | TT.Class | TT.Struct | TT.Interface)
r:=RestOfSpaceDecl(startIndex, t)
{return r;}
};
rule TraitDecl(startIndex::int)::LNode @{
&{@[Hoist] Is($LI, @@trait)} t:=TT.ContextualKeyword
r:=RestOfSpaceDecl(startIndex, t)
{return r;}
};
@[private] rule RestOfSpaceDecl(startIndex::int, kindTok::Token)::LNode @{
{var kind = kindTok.Value -> Symbol;}
name:=ComplexNameDecl
bases:=BaseListOpt
WhereClausesOpt(ref name)
( end:=";"
{return F.Call(kind, name, bases, startIndex, end.EndIndex, kindTok.StartIndex, kindTok.EndIndex);}
| body:=BracedBlock(EcsValidators.KeyNameComponentOf(name))
{return F.Call(kind, LNode.List(name, bases, body),
startIndex, body.Range.EndIndex, kindTok.StartIndex, kindTok.EndIndex);}
)
};
rule AliasDecl(startIndex::int)::LNode
@{
&{@[Hoist] Is($LI, @@alias)} t:=TT.ContextualKeyword
newName:=ComplexNameDecl
("="|":=") oldName:=ComplexNameDecl
result:RestOfAlias(startIndex, t, oldName, newName)
};
rule UsingDirective(startIndex::int, attrs::VList!LNode)::LNode @{
t:TT.Using
( // C# 6 "using static"
&{Is($LI, S.Static)} static_:TT.AttrKeyword
nsName:ExprStart(true) end:";"
{attrs.Add(F.Id(static_));}
/ nsName:ExprStart(true)
( // If the name is like something like `X = Y.Z`, it's really an alias
&{@[Local] nsName.Calls(S.Assign, 2)}
{aliasedType::LNode = nsName.Args[1, F.Missing];}
{nsName = nsName.Args[0, F.Missing];}
r:=RestOfAlias(startIndex, t, aliasedType, nsName)
{return r.WithAttrs(attrs).PlusAttr(_filePrivate);}
/ end:";"
/ error {Error("Expected ';'");}
)
)
{return F.Call(S.Import, nsName, t.StartIndex, end.EndIndex, t.StartIndex, t.EndIndex).WithAttrs(attrs);}
};
rule RestOfAlias(startIndex::int, aliasTok::Token, oldName::LNode, newName::LNode)::LNode @{
bases:=BaseListOpt
WhereClausesOpt(ref newName)
{var name = F.Call(S.Assign, newName, oldName, newName.Range.StartIndex, oldName.Range.EndIndex);}
( end:=";"
{return F.Call(S.Alias, name, bases, startIndex, end.EndIndex, aliasTok.StartIndex, aliasTok.EndIndex);}
| body:=BracedBlock(EcsValidators.KeyNameComponentOf(newName))
{return F.Call(S.Alias, LNode.List(name, bases, body),
startIndex, body.Range.EndIndex, aliasTok.StartIndex, aliasTok.EndIndex);}
)
};
@[private] rule EnumDecl(startIndex::int)::LNode @{
kw:=TT.Enum
name:=ComplexNameDecl
bases:=BaseListOpt
( end:=";"
{return F.Call(kw, name, bases, startIndex, end.EndIndex);}
| lb:="{" rb:="}"
{
var list = ExprListInside(lb, true);
var body = F.Braces(list, lb.StartIndex, rb.EndIndex);
return F.Call(kw, LNode.List(name, bases, body), startIndex, body.Range.EndIndex);
}
)
};
@[private] rule BaseListOpt::LNode @{
( {var bases = (new VList!LNode());}
":" bases+=DataType
("," bases+=DataType)*
{return F.List(bases);}
| {return F.List();}
)
};
@[private] rule WhereClausesOpt(ref name::LNode)
@{
{var list = (new BMultiMap!(Symbol, LNode));}
[list+=WhereClause]*
{ if (list.Count != 0) {
if (!name.CallsMin(S.Of, 2)) {
Error("'{0}' is not generic and cannot use 'where' clauses.", name.ToString())
} else {
var tparams = name.Args.ToWList();
for (i::int = 1; i < tparams.Count; i++) {
var wheres = list[TParamSymbol(tparams[i])];
tparams[i] = tparams[i].PlusAttrs(wheres);
wheres.Clear();
};
name = name.WithArgs(tparams.ToVList());
if (list.Count > 0) {
Error(list[0].Value, "There is no type parameter named '{0}'", list[0].Key);
};
};
};
}
};
fn TParamSymbol(T::LNode)::Symbol
{
if T.IsId
(return T.Name)
else (if T.Calls(S.Substitute, 1) && T.Args[0].IsId
(return T.Args[0].Name)
else
(return S.Missing));
};
@[private] rule WhereClause::KeyValuePair!(Symbol, LNode) @{
&{@[Hoist] Is($LI, @@where)} where:=TT.LinqKeyword T:=(TT.Id|TT.ContextualKeyword|TT.LinqKeyword) ":"
{var constraints = VList!LNode.Empty;}
constraints+=WhereConstraint
("," constraints+=WhereConstraint)*
{return (new KeyValuePair!(Symbol, LNode)(T.Value -> Symbol,
F.Call(S.Where, constraints, where.StartIndex, constraints.Last.Range.EndIndex, where.StartIndex, where.EndIndex)));}
};
@[private] rule WhereConstraint::LNode @{
( t:=(TT.Class | TT.Struct) {return F.Id(t);}
| newkw:=TT.New &{LT($LI).Count == 0} lp:="(" rp:=")"
{return F.Call(newkw, newkw.StartIndex, rp.EndIndex);}
| t:=DataType {return t;} )
};
// ---------------------------------------------------------------------
// -- assembly or module attribute -------------------------------------
// ---------------------------------------------------------------------
@[recognizer(fn Scan_AsmOrModLabel()), private] // recognizer used by AssemblyOrModuleAttribute
rule AsmOrModLabel::Token @{
&{@[Hoist] LT($LI).Value==@@assembly || LT($LI).Value==@@module} t:=TT.ContextualKeyword ":"
{return t;}
};
@[private] rule AssemblyOrModuleAttribute(startIndex::int, attrs::VList!LNode)::LNode
@{
&{@[Hoist] Down($LI) && Up(Try_Scan_AsmOrModLabel(0))} lb:="[" rb:="]"
(_* => {Down(lb);}
kind:=AsmOrModLabel
{var list = (new VList!LNode());}
ExprList(ref list)
{
Up();
var r = F.Call(kind.Value == @@module ? S.Module : S.Assembly, list, startIndex, rb.EndIndex, kind.StartIndex, kind.EndIndex);
return r.WithAttrs(attrs);
}
)
};
// ---------------------------------------------------------------------
// methods, properties, variable/field declarations, operators ---------
// ---------------------------------------------------------------------
@[private] rule MethodOrPropertyOrVar(startIndex::int, attrs::VList!LNode)::LNode @{
{isExtensionMethod::bool = false; isNamedThis::bool;}
[ t:=TT.This {attrs.Add(F.Id(t)); isExtensionMethod = true;} ]?
type:=DataType
name:MethodOrPropertyName(!isExtensionMethod, out isNamedThis)
( // Method declaration/definition
&{!isNamedThis}
result:MethodArgListAndBody(startIndex, type.Range.StartIndex, attrs, S.Fn, type, name)
{return $result;} // don't add attributes again
| // Property definition
&!{@["Invalid property name"] name.IsLiteral}
result:RestOfPropertyDefinition(startIndex, type, name, false)
| // Variable declaration statement
&{!isNamedThis}
&!{@["Invalid variable name"] name.IsLiteral}
{MaybeRecognizeVarAsKeyword(ref type);}
{var parts = LNode.List(type);}
{var isArray = IsArrayType(type);}
parts+=VarInitializerOpt(name, isArray)
["," name:ComplexNameDecl
parts+=VarInitializerOpt(name, isArray)]*
end:=";"
{var typeStart = type.Range.StartIndex;}
{$result = F.Call(S.Var, parts, startIndex, end.EndIndex, typeStart, typeStart);}
| error {Error("Syntax error in what appears to be a method, property, or variable declaration");}
ScanToEndOfStmt
{$result = F.Call(S.Var, type, name, type.Range.StartIndex, name.Range.EndIndex);}
)
{$result = $result.PlusAttrs(attrs);}
};
@[private] rule VarInitializerOpt(name::LNode, isArray::bool)::LNode @{
[ {eqIndex::int = LT0.StartIndex;}
expr:VarInitializer(isArray)
{return F.Call(S.Assign, name, expr, name.Range.StartIndex, expr.Range.EndIndex, eqIndex, eqIndex + 1);}]?
{return name;}
};
@[private] rule VarInitializer(isArray::bool)::LNode @{
("="|":=")
// The initializer for an array in C# can be a braced list of expressions.
// EC# also allows braced blocks as expressions, so I'll distinguish the
// cases by checking for semicolons. If there's a ";", it's an EC# block.
( &{@[Local] isArray}
&{@[Local] Down($LI) && Up(HasNoSemicolons())}
lb:="{" rb:="}"
{
var initializers = InitializerListInside(lb);
$result = F.Call(S.ArrayInit, initializers, lb.StartIndex, rb.EndIndex,
lb.StartIndex, lb.EndIndex, NodeStyle.Expression);
}
/ result:ExprStart(false)
)
};
@[private] rule RestOfPropertyDefinition(startIndex::int, type::LNode, name::LNode, isExpression::bool)::LNode @{
{args::LNode = F.Missing;}
[lb:"[" rb:"]" {args = ArgList(lb, rb);}]?
WhereClausesOpt(ref name)
{initializer::LNode;}
body:=MethodBodyOrForward(true, out initializer, isExpression)
{
var parts = (new VList!LNode() { type; name; args; body });
if initializer != null { parts.Add(initializer); };
targetIndex::int = type.Range.StartIndex;
$result = F.Call(S.Property, parts, startIndex, body.Range.EndIndex, targetIndex, targetIndex);
}
};
@[private] rule OperatorCastMethod(startIndex::int, attrs::VList!LNode)::LNode @{
{r::LNode;}
op:=TT.Operator type:=DataType
{var name = F.Attr(_triviaUseOperatorKeyword, F.Id(S.Cast, op.StartIndex, op.EndIndex));}
r=MethodArgListAndBody(startIndex, op.StartIndex, attrs, S.Fn, type, name)
{return r;}
};
@[private] rule MethodArgListAndBody(startIndex::int, targetIndex::int, attrs::VList!LNode, kind::Symbol, type::LNode, name::LNode)::LNode @{
lp:="(" rp:=")"
WhereClausesOpt(ref name)
{var(r::LNode, _::LNode, baseCall::LNode = null, consCallIndex::int = -1); }
( ":" target:=(TT.This | TT.Base) baselp:="(" baserp:=")"
{
baseCall = F.Call(target.Value -> Symbol, ExprListInside(baselp), target.StartIndex, baserp.EndIndex, target.StartIndex, target.EndIndex);
if (kind != S.Constructor) {
Error(baseCall, "This is not a constructor declaration, so there should be no ':' clause.");
};
consCallIndex = $":".StartIndex;
}
)?
{ // Check for [return:] attributes and transfer them to the return type
for (i::int = 0; i < attrs.Count; i++) {
var attr = attrs[i];
if IsNamedArg(attr) && attr.Args[0].IsIdNamed(S.Return) {
type = type.PlusAttr(attr.Args[1]);
attrs.RemoveAt(i);
i--;
};
};
}
( default end:=";"
{
if kind == S.Constructor && baseCall != null {
Error(baseCall, "A method body is required.");
// Behave as though the method body were present.
var parts = LNode.List(type, name, ArgList(lp, rp), LNode.Call(S.Braces, new VList!LNode(baseCall), baseCall.Range));
r = F.Call(kind, parts, startIndex, baseCall.Range.EndIndex, targetIndex, targetIndex);
} else {
var parts = LNode.List(type, name, ArgList(lp, rp));
r = F.Call(kind, parts, startIndex, end.EndIndex, targetIndex, targetIndex);
};
}
| body:=MethodBodyOrForward(false, out _, false, consCallIndex)
{
if kind == S.Delegate { Error("A 'delegate' is not expected to have a method body."); };
if baseCall != null {
if (!body.Calls(S.Braces)) {
body = F.Braces(LNode.List(body), startIndex, body.Range.EndIndex);
};
body = body.WithArgs(body.Args.Insert(0, baseCall));
};
var parts = (new VList!LNode() { type; name; ArgList(lp, rp); body });
r = F.Call(kind, parts, startIndex, body.Range.EndIndex, targetIndex, targetIndex);
}
)
{return r.PlusAttrs(attrs);}
};
@[private] fn MethodBodyOrForward()::LNode { _::LNode; return MethodBodyOrForward(@false, out _); };
@[private] rule MethodBodyOrForward(isProperty::bool, out propInitializer::LNode, isExpression::bool = false, bodyStartIndex::int = -1)::LNode @{
{propInitializer = null;}
( op:="==>" e:=ExprStart(true) SemicolonIf(!isExpression) {return F.Call(op, e, op.StartIndex, e.Range.EndIndex);}
| op:="=>" e:=ExprStart(false) SemicolonIf(!isExpression) {return e;} // C# 6 lambda-style function
| e:=TokenLiteral [&{!isExpression} ";"]? {return e;}
| body:=BracedBlock(S.Fn, null, bodyStartIndex)
// TODO: figure out why the hell LLLPG thinks there's an ambiguity here
greedy[ // C# 6 property initializer
&{@[Local] isProperty} "=" propInitializer=ExprStart(false)
SemicolonIf(!isExpression)
]?
{ return body; }
)
};
@[private] token SemicolonIf(isStatement::bool) @{
(";"|","|EOF) => // remember that ')','}',']' counts as EOF
( &{isStatement} ";"
/ {if isStatement {Error(0, "Expected ';' to end statement");};}
)
};
fn IsArrayType(type::LNode)::bool
{
// Detect an array type, which has the form #of(@`'[,]`, Type)
return type.Calls(S.Of, 2) && S.IsArrayKeyword(type.Args[0].Name);
};
fn ArgList(lp::Token, rp::Token)::LNode
{
var list = (new VList!LNode());
if (Down(lp.Children)) {
ArgList(ref list);
Up();
};
return F.Call(S.AltList, list, lp.StartIndex, rp.EndIndex, lp.StartIndex, lp.StartIndex + 1);
};
@[recognizer(fn HasNoSemicolons()::bool), private]
rule NoSemicolons @{ ~";"* EOF };
// ---------------------------------------------------------------------
// Constructor/destructor ----------------------------------------------
// ---------------------------------------------------------------------
@[private] rule Constructor(startIndex::int, attrs::VList!LNode)::LNode @{
{r::LNode; n::Token;}
( &{@[Hoist] _spaceName == LT($LI).Value}
n=(TT.Id|TT.ContextualKeyword|TT.LinqKeyword)
&("(" ")" ("{" | ";"))
/ &{@[Hoist] _spaceName != S.Fn || LA($LI+3) == TT.LBrace}
n=TT.This
&("(" ")" ("{" | ";"))
/ n=(TT.Id|TT.ContextualKeyword|TT.LinqKeyword|TT.This)
&("(" ")" ":")
)
{name::LNode = F.Id(n.Value -> Symbol, n.StartIndex, n.EndIndex);}
r=MethodArgListAndBody(startIndex, n.StartIndex, attrs, S.Constructor, F.Missing, name)
{return r;}
};
@[private] rule Destructor(startIndex::int, attrs::VList!LNode)::LNode @{
tilde:="~"
n:=(TT.Id|TT.ContextualKeyword|TT.LinqKeyword|TT.This)
{ var name = n.Value -> Symbol;
if name != _spaceName {
Error("Unexpected destructor '{0}'", name);
};
name2::LNode = F.Call(tilde, F.Id(n), tilde.StartIndex, n.EndIndex);
}
result:MethodArgListAndBody(startIndex, tilde.StartIndex, attrs, S.Fn, F.Missing, name2)
};
// ---------------------------------------------------------------------
// Delegate & event declarations ---------------------------------------
// ---------------------------------------------------------------------
@[private] rule DelegateDecl(startIndex::int, attrs::VList!LNode)::LNode @{
d:=TT.Delegate type:=DataType name:=ComplexNameDecl
r:=MethodArgListAndBody(startIndex, d.StartIndex, attrs, S.Delegate, type, name)
{return r.WithAttrs(attrs);}
};
@[private] rule EventDecl(startIndex::int)::LNode @{
// Event names can be complex identifiers in case they are explicit interface impls (IFoo.Event)
eventkw:TT.Event type:=DataType name:=ComplexNameDecl
[ {var parts = (new VList!LNode(name));}
( "," parts+=ComplexNameDecl )+
{name = F.List(parts, name.Range.StartIndex, parts.Last.Range.EndIndex);}
]?
( ";"
{$result = F.Call(eventkw, type, name, startIndex, $";".EndIndex);}
| body:=BracedBlock(S.Fn)
{if name.Calls(S.AltList) {Error("A body is not allowed when defining multiple events.")};}
{$result = F.Call(eventkw, LNode.List(type, name, body), startIndex, body.Range.EndIndex);}
)
};
// ---------------------------------------------------------------------
// Statements for executable contexts ----------------------------------
// ---------------------------------------------------------------------
// Labels, default:, case expr: ----------------------------------------
rule LabelStmt(startIndex::int)::LNode @{
id:=(TT.Id|TT.ContextualKeyword|TT.LinqKeyword|TT.Default) end:=":"
{return F.Call(S.Label, F.Id(id), startIndex, end.EndIndex, id.StartIndex, id.StartIndex);}
};
rule CaseStmt(startIndex::int)::LNode @{
{var cases = VList!LNode.Empty;}
kw:=TT.Case cases+=ExprStartNNP(true)
("," cases+=ExprStartNNP(true))* end:=":"
{return F.Call(kw, cases, startIndex, end.EndIndex);}
};
// Block-call statement (e.g. get {...}, unroll(...) {...}) ----------------
// Block-call statements help support properties (get/set), events (add/
// remove) and macros. No semicolon is required at the end, and the
// statement cannot continue afterward (at statement level, foo {y} = z;
// is a syntax error since '}' marks the end of the statement.)
// The expressions in "foo(exprs) {...}" are parsed subtly differently than
// for "foo(exprs);" : unassigned variable declarations are allowed only in
// the former. This enables macro syntax like "on_throw(Exception e) {...}";
// in contrast we MUST NOT parse "Foo(Bar<T> x);" as a variable declaration.
@[private] rule BlockCallStmt(startIndex::int)::LNode @{
id:=(TT.Id|TT.ContextualKeyword|TT.LinqKeyword)
&( "(" ")" ("{" "}" | TT.Id) | "{" "}" | "==>" ) // Used by Stmt to disambiguate
{var args = (new VList!LNode());}
{block::LNode;}
( lp:="(" rp:=")" {args=AppendExprsInside(lp, args, false, true);}
( block=BracedBlock
| TT.Id => block=Stmt
{ // Braces are now required
ErrorSink.Write(Severity.Error, block,
ColumnOf(block.Range.StartIndex) <= ColumnOf(id.StartIndex)
? "Probable missing semicolon before this statement."
: "Probable missing braces around body of '{0}' statement.", id.Value);
}
)
| fwd:="==>" e:=ExprStart(true) ";"
{block = SetOperatorStyle(F.Call(fwd, e, fwd.StartIndex, e.Range.EndIndex));}
| block=BracedBlock
)
{
args.Add(block);
var result = F.Call(id.Value -> Symbol, args, id.StartIndex, block.Range.EndIndex, id.StartIndex, id.EndIndex, NodeStyle.Special);
if block.Calls(S.Forward, 1) {
result = F.Attr(_triviaForwardedProperty, result);
};
return result;
}
};
fn ColumnOf(index::int)::int
{
return _sourceFile.IndexToLine(index).PosInLine;
};
// break, continue, return, throw --------------------------------------
@[LL(1)]
@[private] rule ReturnBreakContinueThrow(startIndex::int)::LNode @{
kw:=(TT.Break | TT.Continue | TT.Return | TT.Throw)
greedy[e:ExprStartNNP(false)]?
{
if e != null (return F.Call(kw.Value -> Symbol, e, startIndex, e.Range.EndIndex, kw.StartIndex, kw.EndIndex))
else (return F.Call(kw.Value -> Symbol, startIndex, kw.EndIndex, kw.StartIndex, kw.EndIndex));
}
};
// goto, goto case -----------------------------------------------------
@[private] rule GotoStmt(startIndex::int)::LNode @{
kw:=TT.Goto
( def:=TT.Default
{return F.Call(kw, F.Id(def), startIndex, kw.EndIndex);}
/
e:=ExprOrNull(false)
{
if e != null (return F.Call(kw, e, startIndex, e.Range.EndIndex))
else (return F.Call(kw, startIndex, kw.EndIndex));
}
)
};
@[private] rule GotoCaseStmt(startIndex::int)::LNode @{
{e::LNode = null;}
kw:=TT.Goto kw2:=TT.Case
( def:=TT.Default
{e = F.Id(S.Default, def.StartIndex, def.EndIndex);}
/ e=ExprStartNNP(false))
{var endIndex = e != null ? e.Range.EndIndex : kw2.EndIndex;}
{return F.Call(S.GotoCase, e, startIndex, endIndex, kw.StartIndex, kw2.EndIndex);}
};
// checked & unchecked -------------------------------------------------
@[private] rule CheckedOrUncheckedStmt(startIndex::int)::LNode @{
kw:=(TT.Checked | TT.Unchecked)
bb:=BracedBlock
{return F.Call(kw.Value -> Symbol, bb, startIndex, bb.Range.EndIndex, kw.StartIndex, kw.EndIndex);}
};
// do-while & while ----------------------------------------------------
@[private] rule DoStmt(startIndex::int)::LNode @{
kw:=TT.Do block:=Stmt TT.While "(" ")" end:=";"
{
var parts = (new VList!LNode(block));
SingleExprInside($"(", "while (...)", @false, ref parts);
return F.Call(S.DoWhile, parts, startIndex, end.EndIndex, kw.StartIndex, kw.EndIndex);
}
};
@[private] rule WhileStmt(startIndex::int)::LNode @{
kw:=TT.While "(" ")" block:=Stmt
{
var cond = SingleExprInside($"(", "while (...)");
return F.Call(kw, cond, block, startIndex, block.Range.EndIndex);
}
};
// for & foreach -------------------------------------------------------
@[private] rule ForStmt(startIndex::int)::LNode @{
kw:=TT.For "(" ")" block:=Stmt
(_* => {Down($"(");})
{var init = VList!LNode.Empty; var inc = init;}
ExprList(ref init, false, true) ";" cond:=ExprOpt(false) ";" ExprList(ref inc, false, false)
(EOF => {Up();})
{
var initL = F.Call(S.AltList, init);
var incL = F.Call(S.AltList, inc);
var parts = (new VList!LNode { initL; cond; incL; block });
return F.Call(kw, parts, startIndex, block.Range.EndIndex);
}
};
@[private] rule ForEachStmt(startIndex::int)::LNode @{
kw:=TT.Foreach p:="(" ")"
block:=Stmt
(=> {Down(p);}
[ &VarIn var:VarIn(out _::Token) ]?
// (=>) removes some unwanted complexity in the prediction decision of VarIn
(=>) expr:=ExprStart(false) (")"|EOF)
{
var parts = LNode.List(var ?? F.Missing, expr, block);
return Up(F.Call(kw, parts, startIndex, block.Range.EndIndex));
}
)
};
// The "T id in" part of "foreach (T id in e)" or "from int x in ..." (type is optional)
@[recognizer(fn Scan_VarIn())]
@[private] rule VarIn(out inTok::Token)::LNode @{
pair:=VarDeclStart
{var start = pair.A.Range.StartIndex;}
{$result = F.Call(S.Var, pair.A, pair.B, start, pair.B.Range.EndIndex, start, start);}
inTok=TT.In
};
// if-else -------------------------------------------------------------
@[private] rule IfStmt(startIndex::int)::LNode @{
{else::LNode = null;}
kw:=TT.If p:="(" ")" then:=Stmt
greedy(TT.Else else=Stmt)?
{
var cond = SingleExprInside(p, "if (...)");
var parts = (else == null ? LNode.List(cond, then) : LNode.List(cond, then, else));
return F.Call(kw, parts, startIndex, then.Range.EndIndex);
}
};
@[private] rule SwitchStmt(startIndex::int)::LNode @{
kw:=TT.Switch p:="(" ")" block:=BracedBlock
{
var expr = SingleExprInside(p, "switch (...)");
return F.Call(kw, expr, block, startIndex, block.Range.EndIndex);
}
};
// using, lock, fixed --------------------------------------------------
@[private] rule UsingStmt(startIndex::int)::LNode @{
kw:=TT.Using p:="(" ")" block:=Stmt
{
var expr = SingleExprInside(p, "using (...)");
return F.Call(S.UsingStmt, expr, block, startIndex, block.Range.EndIndex, kw.StartIndex, kw.EndIndex);
}
};
@[private] rule LockStmt(startIndex::int)::LNode @{
kw:=TT.Lock p:="(" ")" block:=Stmt
{
var expr = SingleExprInside(p, "lock (...)");
return F.Call(kw, expr, block, startIndex, block.Range.EndIndex);
}
};
@[private] rule FixedStmt(startIndex::int)::LNode @{
kw:=TT.Fixed p:="(" ")" block:=Stmt
{
var expr = SingleExprInside(p, "fixed (...)", true);
return F.Call(kw, expr, block, startIndex, block.Range.EndIndex);
}
};
// try -----------------------------------------------------------------
@[private] rule TryStmt(startIndex::int)::LNode @{
trykw:=TT.Try header:=Stmt
{var parts = (new VList!LNode { header });}
{varExpr::LNode; whenExpr::LNode;}
greedy[
kw:=TT.Catch
( p:="(" ")" { varExpr = SingleExprInside(p, "catch (...)", true); }
/ { varExpr = MissingHere(); } )
( &{Is($LI, @@when)} TT.ContextualKeyword
c:="(" ")" { whenExpr = SingleExprInside(c, "when (...)"); }
/ { whenExpr = MissingHere(); } )
handler:Stmt
{parts.Add(F.Call(kw, LNode.List(varExpr, whenExpr, handler), kw.StartIndex, handler.Range.EndIndex));}
]*
greedy[
kw:=TT.Finally handler:Stmt
{parts.Add(F.Call(kw, handler, kw.StartIndex, handler.Range.EndIndex));}
]*
{
var result = F.Call(trykw, parts, startIndex, parts.Last.Range.EndIndex);
if parts.Count == 1 {
Error(result, "'try': At least one 'catch' or 'finally' clause is required");
};
return result;
}
};
// ---------------------------------------------------------------------
// ExprList and StmtList -----------------------------------------------
// ---------------------------------------------------------------------
@[LL(1)]
rule ExprOrNull(allowUnassignedVarDecl::bool = false)::LNode @{
greedy[result:ExprStart(allowUnassignedVarDecl)]?
};
rule ExprOpt(allowUnassignedVarDecl::bool = false)::LNode @{
result:ExprOrNull(allowUnassignedVarDecl)
{result ??= MissingHere();}
};
fn MissingHere()::LNode
{
var i = GetTextPosition(InputPosition);
return F.Id(S.Missing, i, i);
};
rule ExprList(ref list::VList!LNode, allowTrailingComma::bool = false, allowUnassignedVarDecl::bool = false) @{
nongreedy(
list+=ExprOpt(allowUnassignedVarDecl)
( "," &{@[Local] allowTrailingComma} EOF
/ "," list+=ExprOpt(allowUnassignedVarDecl)
| error {Error("'{0}': Syntax error in expression list", CurrentTokenText());}
~","+
)*
)?
// semicolon may terminate in a for-loop; EOF represents ')' in other cases
((EOF|";") =>)
};
rule ArgList(ref list::VList!LNode) @{
nongreedy(
list+=ExprOpt(true)
( "," list+=ExprOpt(true)
| error {Error("Syntax error in argument list");} ~","*
)*
)?
EOF
};
@[LL(3)]
rule InitializerExpr::LNode @{
( lb:="{" rb:="}"
{
var exprs = InitializerListInside(lb);
$result = F.Call(S.Braces, exprs, lb.StartIndex, rb.EndIndex, lb.StartIndex, lb.EndIndex, NodeStyle.Expression);
}
/ lb:="[" "]" eq:"=" e:=ExprStart(false)
{ $result = F.Call(S.InitializerAssignment, ExprListInside(lb).Add(e), lb.StartIndex, e.Range.EndIndex, eq.StartIndex, eq.EndIndex); }
/ result:ExprOpt(false)
)
};
// Used for new int[][] { ... } or int[][] x = { ... }
rule InitializerList(ref list::VList!LNode) @{
nongreedy(
list+=InitializerExpr
( "," EOF
/ "," list+=InitializerExpr
| error {Error("Syntax error in initializer list");} ~","*)*
)?
EOF
};
rule StmtList(ref list::VList!LNode) @{
(_ => list+=Stmt)*
EOF
};
};
};
You can’t perform that action at this time.