@@ -288,10 +288,24 @@ export class IvyParser extends Parser {
288
288
simpleExpressionChecker = IvySimpleExpressionChecker ; //
289
289
}
290
290
291
+ /** Describes a stateful context an expression parser is in. */
292
+ enum ParseContextFlags {
293
+ None = 0 ,
294
+ /**
295
+ * A Writable context is one in which a value may be written to an lvalue.
296
+ * For example, after we see a property access, we may expect a write to the
297
+ * property via the "=" operator.
298
+ * prop
299
+ * ^ possible "=" after
300
+ */
301
+ Writable = 1 ,
302
+ }
303
+
291
304
export class _ParseAST {
292
305
private rparensExpected = 0 ;
293
306
private rbracketsExpected = 0 ;
294
307
private rbracesExpected = 0 ;
308
+ private context = ParseContextFlags . None ;
295
309
296
310
// Cache of expression start and input indeces to the absolute source span they map to, used to
297
311
// prevent creating superfluous source spans in `sourceSpan`.
@@ -368,6 +382,16 @@ export class _ParseAST {
368
382
this . index ++ ;
369
383
}
370
384
385
+ /**
386
+ * Executes a callback in the provided context.
387
+ */
388
+ private withContext < T > ( context : ParseContextFlags , cb : ( ) => T ) : T {
389
+ this . context |= context ;
390
+ const ret = cb ( ) ;
391
+ this . context ^= context ;
392
+ return ret ;
393
+ }
394
+
371
395
consumeOptionalCharacter ( code : number ) : boolean {
372
396
if ( this . next . isCharacter ( code ) ) {
373
397
this . advance ( ) ;
@@ -384,6 +408,12 @@ export class _ParseAST {
384
408
return this . next . isKeywordAs ( ) ;
385
409
}
386
410
411
+ /**
412
+ * Consumes an expected character, otherwise emits an error about the missing expected character
413
+ * and skips over the token stream until reaching a recoverable point.
414
+ *
415
+ * See `this.error` and `this.skip` for more details.
416
+ */
387
417
expectCharacter ( code : number ) {
388
418
if ( this . consumeOptionalCharacter ( code ) ) return ;
389
419
this . error ( `Missing expected ${ String . fromCharCode ( code ) } ` ) ;
@@ -631,18 +661,23 @@ export class _ParseAST {
631
661
result = this . parseAccessMemberOrMethodCall ( result , true ) ;
632
662
633
663
} else if ( this . consumeOptionalCharacter ( chars . $LBRACKET ) ) {
634
- this . rbracketsExpected ++ ;
635
- const key = this . parsePipe ( ) ;
636
- this . rbracketsExpected -- ;
637
- this . expectCharacter ( chars . $RBRACKET ) ;
638
- if ( this . consumeOptionalOperator ( '=' ) ) {
639
- const value = this . parseConditional ( ) ;
640
- result = new KeyedWrite (
641
- this . span ( resultStart ) , this . sourceSpan ( resultStart ) , result , key , value ) ;
642
- } else {
643
- result = new KeyedRead ( this . span ( resultStart ) , this . sourceSpan ( resultStart ) , result , key ) ;
644
- }
645
-
664
+ this . withContext ( ParseContextFlags . Writable , ( ) => {
665
+ this . rbracketsExpected ++ ;
666
+ const key = this . parsePipe ( ) ;
667
+ if ( key instanceof EmptyExpr ) {
668
+ this . error ( `Key access cannot be empty` ) ;
669
+ }
670
+ this . rbracketsExpected -- ;
671
+ this . expectCharacter ( chars . $RBRACKET ) ;
672
+ if ( this . consumeOptionalOperator ( '=' ) ) {
673
+ const value = this . parseConditional ( ) ;
674
+ result = new KeyedWrite (
675
+ this . span ( resultStart ) , this . sourceSpan ( resultStart ) , result , key , value ) ;
676
+ } else {
677
+ result =
678
+ new KeyedRead ( this . span ( resultStart ) , this . sourceSpan ( resultStart ) , result , key ) ;
679
+ }
680
+ } ) ;
646
681
} else if ( this . consumeOptionalCharacter ( chars . $LPAREN ) ) {
647
682
this . rparensExpected ++ ;
648
683
const args = this . parseCallArguments ( ) ;
@@ -994,6 +1029,10 @@ export class _ParseAST {
994
1029
this . consumeOptionalCharacter ( chars . $SEMICOLON ) || this . consumeOptionalCharacter ( chars . $COMMA ) ;
995
1030
}
996
1031
1032
+ /**
1033
+ * Records an error and skips over the token stream until reaching a recoverable point. See
1034
+ * `this.skip` for more details on token skipping.
1035
+ */
997
1036
error ( message : string , index : number | null = null ) {
998
1037
this . errors . push ( new ParserError ( message , this . input , this . locationText ( index ) , this . location ) ) ;
999
1038
this . skip ( ) ;
@@ -1005,25 +1044,32 @@ export class _ParseAST {
1005
1044
`at the end of the expression` ;
1006
1045
}
1007
1046
1008
- // Error recovery should skip tokens until it encounters a recovery point. skip() treats
1009
- // the end of input and a ';' as unconditionally a recovery point. It also treats ')',
1010
- // '}' and ']' as conditional recovery points if one of calling productions is expecting
1011
- // one of these symbols. This allows skip() to recover from errors such as '(a.) + 1' allowing
1012
- // more of the AST to be retained (it doesn't skip any tokens as the ')' is retained because
1013
- // of the '(' begins an '(' <expr> ')' production). The recovery points of grouping symbols
1014
- // must be conditional as they must be skipped if none of the calling productions are not
1015
- // expecting the closing token else we will never make progress in the case of an
1016
- // extraneous group closing symbol (such as a stray ')'). This is not the case for ';' because
1017
- // parseChain() is always the root production and it expects a ';'.
1018
-
1019
- // If a production expects one of these token it increments the corresponding nesting count,
1020
- // and then decrements it just prior to checking if the token is in the input.
1047
+ /**
1048
+ * Error recovery should skip tokens until it encounters a recovery point. skip() treats
1049
+ * the end of input and a ';' as unconditionally a recovery point. It also treats ')',
1050
+ * '}' and ']' as conditional recovery points if one of calling productions is expecting
1051
+ * one of these symbols. This allows skip() to recover from errors such as '(a.) + 1' allowing
1052
+ * more of the AST to be retained (it doesn't skip any tokens as the ')' is retained because
1053
+ * of the '(' begins an '(' <expr> ')' production). The recovery points of grouping symbols
1054
+ * must be conditional as they must be skipped if none of the calling productions are not
1055
+ * expecting the closing token else we will never make progress in the case of an
1056
+ * extraneous group closing symbol (such as a stray ')'). This is not the case for ';' because
1057
+ * parseChain() is always the root production and it expects a ';'.
1058
+ *
1059
+ * Furthermore, the presence of a stateful context can add more recovery points.
1060
+ * - in a `Writable` context, we are able to recover after seeing the `=` operator, which
1061
+ * signals the presence of an independent rvalue expression following the `=` operator.
1062
+ *
1063
+ * If a production expects one of these token it increments the corresponding nesting count,
1064
+ * and then decrements it just prior to checking if the token is in the input.
1065
+ */
1021
1066
private skip ( ) {
1022
1067
let n = this . next ;
1023
1068
while ( this . index < this . tokens . length && ! n . isCharacter ( chars . $SEMICOLON ) &&
1024
1069
( this . rparensExpected <= 0 || ! n . isCharacter ( chars . $RPAREN ) ) &&
1025
1070
( this . rbracesExpected <= 0 || ! n . isCharacter ( chars . $RBRACE ) ) &&
1026
- ( this . rbracketsExpected <= 0 || ! n . isCharacter ( chars . $RBRACKET ) ) ) {
1071
+ ( this . rbracketsExpected <= 0 || ! n . isCharacter ( chars . $RBRACKET ) ) &&
1072
+ ( ! ( this . context & ParseContextFlags . Writable ) || ! n . isOperator ( '=' ) ) ) {
1027
1073
if ( this . next . isError ( ) ) {
1028
1074
this . errors . push (
1029
1075
new ParserError ( this . next . toString ( ) ! , this . input , this . locationText ( ) , this . location ) ) ;
0 commit comments