• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

rokucommunity / brs / #307

14 Jun 2024 01:22PM UTC coverage: 89.069% (-0.3%) from 89.375%
#307

push

web-flow
Added support for multi-dimensional array access in brackets notation (#78)

2131 of 2587 branches covered (82.37%)

Branch coverage included in aggregate %.

54 of 72 new or added lines in 5 files covered. (75.0%)

4 existing lines in 2 files now uncovered.

5993 of 6534 relevant lines covered (91.72%)

28682.06 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.93
/src/parser/Parser.ts
1
import { EventEmitter } from "events";
143✔
2

3
import * as Expr from "./Expression";
143✔
4
type Expression = Expr.Expression;
5
import * as Stmt from "./Statement";
143✔
6
type Statement = Stmt.Statement;
7
import { Lexeme, Token, Identifier, Location, ReservedWords } from "../lexer";
143✔
8
import { ParseError } from "./ParseError";
143✔
9

10
import {
143✔
11
    BrsInvalid,
12
    BrsBoolean,
13
    BrsString,
14
    Int32,
15
    ValueKind,
16
    Argument,
17
    StdlibArgument,
18
} from "../brsTypes";
19

20
/** Set of all keywords that end blocks. */
21
type BlockTerminator =
22
    | Lexeme.ElseIf
23
    | Lexeme.Else
24
    | Lexeme.EndFor
25
    | Lexeme.Next
26
    | Lexeme.EndIf
27
    | Lexeme.EndWhile
28
    | Lexeme.EndSub
29
    | Lexeme.EndFunction
30
    | Lexeme.Newline // possible only in a single-line `if` statement
31
    | Lexeme.Eof // possible only in a single-line `if` statement
32
    | Lexeme.Catch
33
    | Lexeme.EndTry;
34

35
/** The set of operators valid for use in assignment statements. */
36
const assignmentOperators = [
143✔
37
    Lexeme.Equal,
38
    Lexeme.MinusEqual,
39
    Lexeme.PlusEqual,
40
    Lexeme.StarEqual,
41
    Lexeme.SlashEqual,
42
    Lexeme.BackslashEqual,
43
    Lexeme.LeftShiftEqual,
44
    Lexeme.RightShiftEqual,
45
];
46

47
/** List of Lexemes that are permitted as property names. */
48
const allowedProperties = [
143✔
49
    Lexeme.And,
50
    Lexeme.Box,
51
    Lexeme.ContinueFor,
52
    Lexeme.ContinueWhile,
53
    Lexeme.CreateObject,
54
    Lexeme.Dim,
55
    Lexeme.Else,
56
    Lexeme.ElseIf,
57
    Lexeme.End,
58
    Lexeme.EndFunction,
59
    Lexeme.EndFor,
60
    Lexeme.EndIf,
61
    Lexeme.EndSub,
62
    Lexeme.EndWhile,
63
    Lexeme.Eval,
64
    Lexeme.Exit,
65
    Lexeme.ExitFor,
66
    Lexeme.ExitWhile,
67
    Lexeme.False,
68
    Lexeme.For,
69
    Lexeme.ForEach,
70
    Lexeme.Function,
71
    Lexeme.GetGlobalAA,
72
    Lexeme.GetLastRunCompileError,
73
    Lexeme.GetLastRunRunTimeError,
74
    Lexeme.Goto,
75
    Lexeme.If,
76
    Lexeme.Invalid,
77
    Lexeme.Let,
78
    Lexeme.Next,
79
    Lexeme.Not,
80
    Lexeme.ObjFun,
81
    Lexeme.Or,
82
    Lexeme.Pos,
83
    Lexeme.Print,
84
    Lexeme.Rem,
85
    Lexeme.Return,
86
    Lexeme.Step,
87
    Lexeme.Stop,
88
    Lexeme.Sub,
89
    Lexeme.Tab,
90
    Lexeme.To,
91
    Lexeme.True,
92
    Lexeme.Type,
93
    Lexeme.While,
94
];
95

96
/** List of Lexeme that are allowed as local var identifiers. */
97
const allowedIdentifiers = [
143✔
98
    Lexeme.EndFor,
99
    Lexeme.ExitFor,
100
    Lexeme.ForEach,
101
    Lexeme.Try,
102
    Lexeme.Catch,
103
];
104

105
/**
106
 * List of string versions of Lexeme that are NOT allowed as local var identifiers.
107
 * Used to throw more helpful "you can't use a reserved word as an identifier" errors.
108
 */
109
const disallowedIdentifiers = new Set(
143✔
110
    [
111
        Lexeme.And,
112
        Lexeme.Box,
113
        Lexeme.CreateObject,
114
        Lexeme.Dim,
115
        Lexeme.Else,
116
        Lexeme.ElseIf,
117
        Lexeme.End,
118
        Lexeme.EndFunction,
119
        Lexeme.EndIf,
120
        Lexeme.EndSub,
121
        Lexeme.EndWhile,
122
        Lexeme.Eval,
123
        Lexeme.Exit,
124
        Lexeme.ExitWhile,
125
        Lexeme.False,
126
        Lexeme.For,
127
        Lexeme.Function,
128
        Lexeme.GetGlobalAA,
129
        Lexeme.GetLastRunCompileError,
130
        Lexeme.GetLastRunRunTimeError,
131
        Lexeme.Goto,
132
        Lexeme.If,
133
        Lexeme.Invalid,
134
        Lexeme.Let,
135
        Lexeme.Next,
136
        Lexeme.Not,
137
        Lexeme.ObjFun,
138
        Lexeme.Or,
139
        Lexeme.Pos,
140
        Lexeme.Print,
141
        Lexeme.Rem,
142
        Lexeme.Return,
143
        Lexeme.Step,
144
        Lexeme.Sub,
145
        Lexeme.Tab,
146
        Lexeme.To,
147
        Lexeme.True,
148
        Lexeme.Type,
149
        Lexeme.While,
150
    ].map((x) => Lexeme[x].toLowerCase())
5,577✔
151
);
152

153
/** The results of a Parser's parsing pass. */
154
interface ParseResults {
155
    /** The statements produced by the parser. */
156
    statements: Stmt.Statement[];
157
    /** The errors encountered by the Parser. */
158
    errors: ParseError[];
159
}
160

161
export class Parser {
143✔
162
    /** Allows consumers to observe errors as they're detected. */
163
    readonly events = new EventEmitter();
1,723✔
164

165
    /**
166
     * A convenience function, equivalent to `new Parser().parse(toParse)`, that parses an array of
167
     * `Token`s into an abstract syntax tree that can be executed with the `Interpreter`.
168
     * @param toParse the array of tokens to parse
169
     * @returns an array of `Statement` objects that together form the abstract syntax tree of the
170
     *          program
171
     */
172
    static parse(toParse: ReadonlyArray<Token>) {
173
        return new Parser().parse(toParse);
24✔
174
    }
175

176
    /**
177
     * Convenience function to subscribe to the `err` events emitted by `parser.events`.
178
     * @param errorHandler the function to call for every Parser error emitted after subscribing
179
     * @returns an object with a `dispose` function, used to unsubscribe from errors
180
     */
181
    public onError(errorHandler: (err: ParseError) => void) {
182
        this.events.on("err", errorHandler);
1,509✔
183
        return {
1,509✔
184
            dispose: () => {
185
                this.events.removeListener("err", errorHandler);
×
186
            },
187
        };
188
    }
189

190
    /**
191
     * Convenience function to subscribe to a single `err` event emitted by `parser.events`.
192
     * @param errorHandler the function to call for the first Parser error emitted after subscribing
193
     */
194
    public onErrorOnce(errorHandler: (err: ParseError) => void) {
195
        this.events.once("err", errorHandler);
×
196
    }
197

198
    /**
199
     * Parses an array of `Token`s into an abstract syntax tree that can be executed with the `Interpreter`.
200
     * @param toParse the array of tokens to parse
201
     * @returns an array of `Statement` objects that together form the abstract syntax tree of the
202
     *          program
203
     */
204
    parse(toParse: ReadonlyArray<Token>): ParseResults {
205
        let current = 0;
1,709✔
206
        let tokens = toParse;
1,709✔
207

208
        //the depth of the calls to function declarations. Helps some checks know if they are at the root or not.
209
        let functionDeclarationLevel = 0;
1,709✔
210

211
        function isAtRootLevel() {
212
            return functionDeclarationLevel === 0;
42,049✔
213
        }
214

215
        let statements: Statement[] = [];
1,709✔
216

217
        let errors: ParseError[] = [];
1,709✔
218

219
        /**
220
         * Add an error to the parse results.
221
         * @param token - the token where the error occurred
222
         * @param message - the message for this error
223
         * @returns an error object that can be thrown if the calling code needs to abort parsing
224
         */
225
        const addError = (token: Token, message: string) => {
1,709✔
226
            let err = new ParseError(token, message);
40✔
227
            errors.push(err);
40✔
228
            this.events.emit("err", err);
40✔
229
            return err;
40✔
230
        };
231

232
        /**
233
         * Add an error at the given location.
234
         * @param location
235
         * @param message
236
         */
237
        const addErrorAtLocation = (location: Location, message: string) => {
1,709✔
238
            addError({ location: location } as any, message);
6✔
239
        };
240

241
        if (toParse.length === 0) {
1,709!
242
            return {
×
243
                statements: [],
244
                errors: [],
245
            };
246
        }
247

248
        try {
1,709✔
249
            while (!isAtEnd()) {
1,709✔
250
                let dec = declaration();
4,042✔
251
                if (dec) {
4,042✔
252
                    statements.push(dec);
4,028✔
253
                }
254
            }
255

256
            return { statements, errors };
1,708✔
257
        } catch (parseError) {
258
            return {
1✔
259
                statements: [],
260
                errors: errors,
261
            };
262
        }
263

264
        /**
265
         * A simple wrapper around `check` to make tests for a `end` identifier.
266
         * `end` is a keyword, but not reserved, so associative arrays can have properties
267
         * called `end`; the parser takes on this task.
268
         * @returns `true` if the next token is an identifier with text `end`, otherwise `false`
269
         */
270
        function checkEnd() {
271
            return check(Lexeme.Identifier) && peek().text.toLowerCase() === "end";
8,988✔
272
        }
273

274
        function declaration(...additionalTerminators: BlockTerminator[]): Statement | undefined {
275
            try {
26,346✔
276
                let statementSeparators = [Lexeme.Colon];
26,346✔
277

278
                if (additionalTerminators.includes(Lexeme.Newline)) {
26,346✔
279
                    // if this declaration can be terminated with a newline, check for one
280
                    // before other statement separators
281
                    if (check(Lexeme.Newline)) {
42!
282
                        return;
×
283
                    }
284
                } else {
285
                    statementSeparators.push(Lexeme.Newline);
26,304✔
286
                }
287

288
                while (match(...statementSeparators));
26,346✔
289

290
                if (additionalTerminators.includes(Lexeme.Newline)) {
26,346✔
291
                    // if this declaration can be terminated with a newline, check for one
292
                    // _after_ a series of `:`s.
293
                    if (check(Lexeme.Newline)) {
42!
294
                        return;
×
295
                    }
296
                }
297

298
                // if we reached the end, don't attempt to do anything else
299
                if (isAtEnd()) {
26,346!
300
                    return;
×
301
                }
302

303
                try {
26,346✔
304
                    if (functionDeclarationLevel === 0 && check(Lexeme.Sub, Lexeme.Function)) {
26,346✔
305
                        return functionDeclaration(false);
3,677✔
306
                    }
307

308
                    if (checkLibrary()) {
22,669✔
309
                        return libraryStatement();
7✔
310
                    }
311

312
                    // BrightScript is like python, in that variables can be declared without a `var`,
313
                    // `let`, (...) keyword. As such, we must check the token *after* an identifier to figure
314
                    // out what to do with it.
315
                    if (
22,662✔
316
                        check(Lexeme.Identifier, ...allowedIdentifiers) &&
33,834✔
317
                        checkNext(...assignmentOperators)
318
                    ) {
319
                        return assignment(...additionalTerminators);
3,289✔
320
                    }
321

322
                    return statement(...additionalTerminators);
19,373✔
323
                } finally {
324
                    while (match(...statementSeparators));
26,346✔
325
                }
326
            } catch (error) {
327
                synchronize();
21✔
328
                return;
21✔
329
            }
330
        }
331

332
        function functionDeclaration(isAnonymous: true): Expr.Function;
333
        function functionDeclaration(isAnonymous: false): Stmt.Function;
334
        function functionDeclaration(isAnonymous: boolean) {
335
            try {
3,917✔
336
                //certain statements need to know if they are contained within a function body
337
                //so track the depth here
338
                functionDeclarationLevel++;
3,917✔
339
                let startingKeyword = peek();
3,917✔
340
                let isSub = check(Lexeme.Sub);
3,917✔
341
                let functionType = advance();
3,917✔
342
                let name: Identifier;
343
                let returnType: ValueKind;
344
                let leftParen: Token;
345
                let rightParen: Token;
346

347
                if (isSub) {
3,917✔
348
                    returnType = ValueKind.Void;
2,581✔
349
                } else {
350
                    returnType = ValueKind.Dynamic;
1,336✔
351
                }
352

353
                if (isAnonymous) {
3,917✔
354
                    leftParen = consume(
240✔
355
                        `Expected '(' after ${functionType.text}`,
356
                        Lexeme.LeftParen
357
                    );
358
                } else {
359
                    name = consume(
3,677✔
360
                        `Expected ${functionType.text} name after '${functionType.text}'`,
361
                        Lexeme.Identifier
362
                    ) as Identifier;
363
                    leftParen = consume(
3,677✔
364
                        `Expected '(' after ${functionType.text} name`,
365
                        Lexeme.LeftParen
366
                    );
367

368
                    //prevent functions from ending with type designators
369
                    let lastChar = name.text[name.text.length - 1];
3,677✔
370
                    if (["$", "%", "!", "#", "&"].includes(lastChar)) {
3,677✔
371
                        //don't throw this error; let the parser continue
372
                        addError(
8✔
373
                            name,
374
                            `Function name '${name.text}' cannot end with type designator '${lastChar}'`
375
                        );
376
                    }
377
                }
378

379
                let args: Argument[] = [];
3,917✔
380
                if (!check(Lexeme.RightParen)) {
3,917✔
381
                    do {
1,046✔
382
                        if (args.length >= Expr.Call.MaximumArguments) {
1,490!
383
                            throw addError(
×
384
                                peek(),
385
                                `Cannot have more than ${Expr.Call.MaximumArguments} arguments`
386
                            );
387
                        }
388

389
                        args.push(signatureArgument());
1,490✔
390
                    } while (match(Lexeme.Comma));
391
                }
392
                rightParen = advance();
3,917✔
393

394
                let maybeAs = peek();
3,917✔
395
                if (check(Lexeme.Identifier) && maybeAs.text.toLowerCase() === "as") {
3,917✔
396
                    advance();
671✔
397

398
                    let typeToken = advance();
671✔
399
                    let typeString = typeToken.text || "";
671!
400
                    let maybeReturnType = ValueKind.fromString(typeString);
671✔
401

402
                    if (!maybeReturnType) {
671!
403
                        throw addError(
×
404
                            typeToken,
405
                            `Function return type '${typeString}' is invalid`
406
                        );
407
                    }
408

409
                    returnType = maybeReturnType;
671✔
410
                }
411

412
                args.reduce((haveFoundOptional: boolean, arg: Argument) => {
3,917✔
413
                    if (haveFoundOptional && !arg.defaultValue) {
1,490!
414
                        throw addError(
×
415
                            {
416
                                kind: Lexeme.Identifier,
417
                                text: arg.name.text,
418
                                isReserved: ReservedWords.has(arg.name.text),
419
                                location: arg.location,
420
                            },
421
                            `Argument '${arg.name.text}' has no default value, but comes after arguments with default values`
422
                        );
423
                    }
424

425
                    return haveFoundOptional || !!arg.defaultValue;
1,490✔
426
                }, false);
427

428
                checkOrThrow(
3,917✔
429
                    `Expected newline or ':' after ${functionType.text} signature`,
430
                    Lexeme.Newline,
431
                    Lexeme.Colon
432
                );
433
                //support ending the function with `end sub` OR `end function`
434
                let maybeBody = block(Lexeme.EndSub, Lexeme.EndFunction);
3,917✔
435
                if (!maybeBody) {
3,917!
436
                    throw addError(
×
437
                        peek(),
438
                        `Expected 'end ${functionType.text}' to terminate ${functionType.text} block`
439
                    );
440
                }
441
                let endingKeyword = maybeBody.closingToken;
3,917✔
442
                let expectedEndKind = isSub ? Lexeme.EndSub : Lexeme.EndFunction;
3,917✔
443

444
                //if `function` is ended with `end sub`, or `sub` is ended with `end function`, then
445
                //add an error but don't hard-fail so the AST can continue more gracefully
446
                if (endingKeyword.kind !== expectedEndKind) {
3,917✔
447
                    addError(
2✔
448
                        endingKeyword,
449
                        `Expected 'end ${functionType.text}' to terminate ${functionType.text} block`
450
                    );
451
                }
452

453
                let func = new Expr.Function(
3,917✔
454
                    args,
455
                    returnType,
456
                    maybeBody.body,
457
                    startingKeyword,
458
                    endingKeyword
459
                );
460

461
                if (isAnonymous) {
3,917✔
462
                    return func;
240✔
463
                } else {
464
                    // only consume trailing newlines in the statement context; expressions
465
                    // expect to handle their own trailing whitespace
466
                    while (match(Lexeme.Newline));
3,677✔
467
                    return new Stmt.Function(name!, func);
3,677✔
468
                }
469
            } finally {
470
                functionDeclarationLevel--;
3,917✔
471
            }
472
        }
473

474
        function signatureArgument(): Argument {
475
            if (!check(Lexeme.Identifier)) {
1,490!
476
                throw addError(
×
477
                    peek(),
478
                    `Expected argument name, but received '${peek().text || ""}'`
×
479
                );
480
            }
481

482
            let name = advance();
1,490✔
483
            let type: ValueKind = ValueKind.Dynamic;
1,490✔
484
            let typeToken: Token | undefined;
485
            let defaultValue;
486

487
            // parse argument default value
488
            if (match(Lexeme.Equal)) {
1,490✔
489
                // it seems any expression is allowed here -- including ones that operate on other arguments!
490
                defaultValue = expression();
82✔
491
            }
492

493
            let next = peek();
1,490✔
494
            if (check(Lexeme.Identifier) && next.text && next.text.toLowerCase() === "as") {
1,490✔
495
                // 'as' isn't a reserved word, so it can't be lexed into an As token without the lexer
496
                // understanding language context.  That's less than ideal, so we'll have to do some
497
                // more intelligent comparisons to detect the 'as' sometimes-keyword here.
498
                advance();
1,162✔
499

500
                typeToken = advance();
1,162✔
501
                let typeValueKind = ValueKind.fromString(typeToken.text);
1,162✔
502

503
                if (!typeValueKind) {
1,162!
504
                    throw addError(
×
505
                        typeToken,
506
                        `Function parameter '${name.text}' is of invalid type '${typeToken.text}'`
507
                    );
508
                }
509

510
                type = typeValueKind;
1,162✔
511
            }
512

513
            return {
1,490✔
514
                name: name,
515
                type: {
516
                    kind: type,
517
                    location: typeToken ? typeToken.location : StdlibArgument.InternalLocation,
1,490✔
518
                },
519
                defaultValue: defaultValue,
520
                location: {
521
                    file: name.location.file,
522
                    start: name.location.start,
523
                    end: typeToken ? typeToken.location.end : name.location.end,
1,490✔
524
                },
525
            };
526
        }
527

528
        function assignment(...additionalterminators: Lexeme[]): Stmt.Assignment {
529
            let name = advance() as Identifier;
3,311✔
530
            //add error if name is a reserved word that cannot be used as an identifier
531
            if (disallowedIdentifiers.has(name.text.toLowerCase())) {
3,311✔
532
                //don't throw...this is fully recoverable
533
                addError(name, `Cannot use reserved word "${name.text}" as an identifier`);
1✔
534
            }
535
            let operator = consume(
3,311✔
536
                `Expected operator ('=', '+=', '-=', '*=', '/=', '\\=', '^=', '<<=', or '>>=') after idenfifier '${name.text}'`,
537
                ...assignmentOperators
538
            );
539

540
            let value = expression();
3,311✔
541
            if (!check(...additionalterminators)) {
3,310✔
542
                consume(
3,277✔
543
                    "Expected newline or ':' after assignment",
544
                    Lexeme.Newline,
545
                    Lexeme.Colon,
546
                    Lexeme.Eof,
547
                    ...additionalterminators
548
                );
549
            }
550

551
            if (operator.kind === Lexeme.Equal) {
3,310✔
552
                return new Stmt.Assignment({ equals: operator }, name, value);
3,295✔
553
            } else {
554
                return new Stmt.Assignment(
15✔
555
                    { equals: operator },
556
                    name,
557
                    new Expr.Binary(new Expr.Variable(name), operator, value)
558
                );
559
            }
560
        }
561

562
        function checkLibrary() {
563
            let isLibraryIdentifier =
564
                check(Lexeme.Identifier) && peek().text.toLowerCase() === "library";
42,042✔
565
            //if we are at the top level, any line that starts with "library" should be considered a library statement
566
            if (isAtRootLevel() && isLibraryIdentifier) {
42,042✔
567
                return true;
6✔
568
            }
569
            //not at root level, library statements are all invalid here, but try to detect if the tokens look
570
            //like a library statement (and let the libraryStatement function handle emitting the errors)
571
            else if (isLibraryIdentifier && checkNext(Lexeme.String)) {
42,036✔
572
                return true;
1✔
573
            }
574
            //definitely not a library statement
575
            else {
576
                return false;
42,035✔
577
            }
578
        }
579

580
        function statement(...additionalterminators: BlockTerminator[]): Statement | undefined {
581
            if (checkLibrary()) {
19,373!
582
                return libraryStatement();
×
583
            }
584

585
            if (check(Lexeme.Stop)) {
19,373✔
586
                return stopStatement();
2✔
587
            }
588

589
            if (check(Lexeme.Try)) {
19,371✔
590
                return tryCatch();
9✔
591
            }
592

593
            if (check(Lexeme.If)) {
19,362✔
594
                return ifStatement();
260✔
595
            }
596

597
            if (check(Lexeme.Print)) {
19,102✔
598
                return printStatement(...additionalterminators);
9,996✔
599
            }
600

601
            if (check(Lexeme.While)) {
9,106✔
602
                return whileStatement();
15✔
603
            }
604

605
            if (check(Lexeme.ContinueWhile)) {
9,091✔
606
                return continueWhile();
1✔
607
            }
608

609
            if (check(Lexeme.ExitWhile)) {
9,090✔
610
                return exitWhile();
2✔
611
            }
612

613
            if (check(Lexeme.For)) {
9,088✔
614
                return forStatement();
22✔
615
            }
616

617
            if (check(Lexeme.ForEach)) {
9,066✔
618
                return forEachStatement();
70✔
619
            }
620

621
            if (check(Lexeme.ContinueFor)) {
8,996✔
622
                return continueFor();
1✔
623
            }
624

625
            if (check(Lexeme.ExitFor)) {
8,995✔
626
                return exitFor();
7✔
627
            }
628

629
            if (checkEnd()) {
8,988✔
630
                return endStatement();
1✔
631
            }
632

633
            if (match(Lexeme.Return)) {
8,987✔
634
                return returnStatement();
1,176✔
635
            }
636

637
            if (match(Lexeme.Throw)) {
7,811✔
638
                return throwStatement();
2✔
639
            }
640

641
            if (check(Lexeme.Dim)) {
7,809✔
642
                return dimStatement();
3✔
643
            }
644

645
            if (check(Lexeme.Goto)) {
7,806✔
646
                return gotoStatement();
3✔
647
            }
648

649
            //does this line look like a label? (i.e.  `someIdentifier:` )
650
            if (check(Lexeme.Identifier) && checkNext(Lexeme.Colon)) {
7,803✔
651
                return labelStatement();
2✔
652
            }
653

654
            // TODO: support multi-statements
655
            return setStatement(...additionalterminators);
7,801✔
656
        }
657

658
        function tryCatch(): Stmt.TryCatch {
659
            let tryKeyword = advance();
9✔
660
            let tryBlock = block(Lexeme.Catch);
9✔
661
            if (!tryBlock) {
9✔
662
                throw addError(peek(), "Expected 'catch' to terminate try block");
1✔
663
            }
664

665
            if (!check(Lexeme.Identifier)) {
8✔
666
                // defer this error so we can parse the `catch` block.
667
                // it'll be thrown if the catch block parses successfully otherwise.
668
                throw addError(peek(), "Expected variable name for caught error after 'catch'");
1✔
669
            }
670

671
            let caughtVariable = new Expr.Variable(advance() as Identifier);
7✔
672
            let catchBlock = block(Lexeme.EndTry);
7✔
673
            if (!catchBlock) {
7✔
674
                throw addError(peek(), "Expected 'end try' or 'endtry' to terminate catch block");
1✔
675
            }
676

677
            return new Stmt.TryCatch(tryBlock.body, catchBlock.body, caughtVariable, {
6✔
678
                try: tryKeyword,
679
                catch: tryBlock.closingToken,
680
                endtry: catchBlock.closingToken,
681
            });
682
        }
683

684
        function whileStatement(): Stmt.While {
685
            const whileKeyword = advance();
15✔
686
            const condition = expression();
15✔
687

688
            checkOrThrow(
15✔
689
                "Expected newline or ':' after 'while ...condition...'",
690
                Lexeme.Newline,
691
                Lexeme.Colon
692
            );
693
            const maybeWhileBlock = block(Lexeme.EndWhile);
15✔
694
            if (!maybeWhileBlock) {
15!
695
                throw addError(peek(), "Expected 'end while' to terminate while-loop block");
×
696
            }
697

698
            return new Stmt.While(
15✔
699
                { while: whileKeyword, endWhile: maybeWhileBlock.closingToken },
700
                condition,
701
                maybeWhileBlock.body
702
            );
703
        }
704

705
        function continueWhile(): Stmt.ContinueWhile {
706
            let keyword = advance();
1✔
707
            checkOrThrow("Expected newline after 'continue while'", Lexeme.Newline);
1✔
708
            return new Stmt.ContinueWhile({ continueWhile: keyword });
1✔
709
        }
710

711
        function exitWhile(): Stmt.ExitWhile {
712
            let keyword = advance();
2✔
713
            checkOrThrow("Expected newline after 'exit while'", Lexeme.Newline);
2✔
714
            return new Stmt.ExitWhile({ exitWhile: keyword });
2✔
715
        }
716

717
        function forStatement(): Stmt.For {
718
            const forKeyword = advance();
22✔
719
            const initializer = assignment(Lexeme.To);
22✔
720
            const to = advance();
22✔
721
            const finalValue = expression();
22✔
722
            let increment: Expression | undefined;
723
            let step: Token | undefined;
724

725
            if (check(Lexeme.Step)) {
22✔
726
                step = advance();
6✔
727
                increment = expression();
6✔
728
            } else {
729
                // BrightScript for/to/step loops default to a step of 1 if no `step` is provided
730
                increment = new Expr.Literal(new Int32(1), peek().location);
16✔
731
            }
732

733
            let maybeBody = block(Lexeme.EndFor, Lexeme.Next);
22✔
734
            if (!maybeBody) {
22!
735
                throw addError(peek(), "Expected 'end for' or 'next' to terminate for-loop block");
×
736
            }
737

738
            // WARNING: BrightScript doesn't delete the loop initial value after a for/to loop! It just
739
            // stays around in scope with whatever value it was when the loop exited.
740
            return new Stmt.For(
22✔
741
                {
742
                    for: forKeyword,
743
                    to: to,
744
                    step: step,
745
                    endFor: maybeBody.closingToken,
746
                },
747
                initializer,
748
                finalValue,
749
                increment,
750
                maybeBody.body
751
            );
752
        }
753

754
        function forEachStatement(): Stmt.ForEach {
755
            let forEach = advance();
70✔
756
            let name = advance();
70✔
757

758
            let maybeIn = peek();
70✔
759
            if (check(Lexeme.Identifier) && maybeIn.text.toLowerCase() === "in") {
70!
760
                advance();
70✔
761
            } else {
762
                throw addError(maybeIn, "Expected 'in' after 'for each <name>'");
×
763
            }
764

765
            let target = expression();
70✔
766
            if (!target) {
70!
767
                throw addError(peek(), "Expected target object to iterate over");
×
768
            }
769
            advance();
70✔
770

771
            let maybeBody = block(Lexeme.EndFor, Lexeme.Next);
70✔
772
            if (!maybeBody) {
70!
773
                throw addError(peek(), "Expected 'end for' or 'next' to terminate for-loop block");
×
774
            }
775

776
            return new Stmt.ForEach(
70✔
777
                {
778
                    forEach: forEach,
779
                    in: maybeIn,
780
                    endFor: maybeBody.closingToken,
781
                },
782
                name,
783
                target,
784
                maybeBody.body
785
            );
786
        }
787

788
        function continueFor(): Stmt.ContinueFor {
789
            let keyword = advance();
1✔
790
            checkOrThrow("Expected newline after 'continue for'", Lexeme.Newline);
1✔
791
            return new Stmt.ContinueFor({ continueFor: keyword });
1✔
792
        }
793

794
        function exitFor(): Stmt.ExitFor {
795
            let keyword = advance();
7✔
796
            checkOrThrow("Expected newline after 'exit for'", Lexeme.Newline);
7✔
797
            return new Stmt.ExitFor({ exitFor: keyword });
7✔
798
        }
799

800
        function libraryStatement(): Stmt.Library | undefined {
801
            let libraryStatement = new Stmt.Library({
7✔
802
                library: advance(),
803
                //grab the next token only if it's a string
804
                filePath: check(Lexeme.String) ? advance() : undefined,
7✔
805
            });
806

807
            //no token following library keyword token
808
            if (!libraryStatement.tokens.filePath && check(Lexeme.Newline, Lexeme.Colon)) {
7✔
809
                addErrorAtLocation(
1✔
810
                    libraryStatement.tokens.library.location,
811
                    `Missing string literal after ${libraryStatement.tokens.library.text} keyword`
812
                );
813
            }
814
            //does not have a string literal as next token
815
            else if (!libraryStatement.tokens.filePath && peek().kind === Lexeme.Newline) {
6!
816
                addErrorAtLocation(
×
817
                    peek().location,
818
                    `Expected string literal after ${libraryStatement.tokens.library.text} keyword`
819
                );
820
            }
821

822
            //consume all tokens until the end of the line
823
            let invalidTokens = consumeUntil(Lexeme.Newline, Lexeme.Eof, Lexeme.Colon);
7✔
824

825
            if (invalidTokens.length > 0) {
7✔
826
                //add an error for every invalid token
827
                for (let invalidToken of invalidTokens) {
1✔
828
                    addErrorAtLocation(
3✔
829
                        invalidToken.location,
830
                        `Found unexpected token '${invalidToken.text}' after library statement`
831
                    );
832
                }
833
            }
834

835
            //libraries must be at the very top of the file before any other declarations.
836
            let isAtTopOfFile = true;
7✔
837
            for (let statement of statements) {
7✔
838
                //if we found a non-library statement, this statement is not at the top of the file
839
                if (!(statement instanceof Stmt.Library)) {
2✔
840
                    isAtTopOfFile = false;
1✔
841
                }
842
            }
843

844
            //libraries must be a root-level statement (i.e. NOT nested inside of functions)
845
            if (!isAtRootLevel() || !isAtTopOfFile) {
7✔
846
                addErrorAtLocation(
2✔
847
                    libraryStatement.location,
848
                    "Library statements may only appear at the top of a file"
849
                );
850
            }
851
            //consume to the next newline, eof, or colon
852
            while (match(Lexeme.Newline, Lexeme.Eof, Lexeme.Colon));
7✔
853
            return libraryStatement;
7✔
854
        }
855

856
        function ifStatement(): Stmt.If {
857
            const ifToken = advance();
260✔
858
            const startingLine = ifToken.location;
260✔
859

860
            const condition = expression();
260✔
861
            let thenBranch: Stmt.Block;
862
            let elseIfBranches: Stmt.ElseIf[] = [];
258✔
863
            let elseBranch: Stmt.Block | undefined;
864

865
            let thenToken: Token | undefined;
866
            let elseIfTokens: Token[] = [];
258✔
867
            let endIfToken: Token | undefined;
868
            let elseToken: Token | undefined;
869

870
            /**
871
             * A simple wrapper around `check`, to make tests for a `then` identifier.
872
             * As with many other words, "then" is a keyword but not reserved, so associative
873
             * arrays can have properties called "then".  It's a valid identifier sometimes, so the
874
             * parser has to take on the burden of understanding that I guess.
875
             * @returns `true` if the next token is an identifier with text "then", otherwise `false`.
876
             */
877
            function checkThen() {
878
                return check(Lexeme.Identifier) && peek().text.toLowerCase() === "then";
274✔
879
            }
880

881
            if (checkThen()) {
258✔
882
                // `then` is optional after `if ...condition...`, so only advance to the next token if `then` is present
883
                thenToken = advance();
116✔
884
            }
885

886
            if (check(Lexeme.Newline) || check(Lexeme.Colon)) {
258✔
887
                //keep track of the current error count, because if the then branch fails,
888
                //we will trash them in favor of a single error on if
889
                let errorsLengthBeforeBlock = errors.length;
231✔
890

891
                // we're parsing a multi-line ("block") form of the BrightScript if/then/else and must find
892
                // a trailing "end if"
893

894
                let maybeThenBranch = block(Lexeme.EndIf, Lexeme.Else, Lexeme.ElseIf);
231✔
895
                if (!maybeThenBranch) {
231✔
896
                    //throw out any new errors created as a result of a `then` block parse failure.
897
                    //the block() function will discard the current line, so any discarded errors will
898
                    //resurface if they are legitimate, and not a result of a malformed if statement
899
                    errors.splice(errorsLengthBeforeBlock, errors.length - errorsLengthBeforeBlock);
2✔
900

901
                    //this whole if statement is bogus...add error to the if token and hard-fail
902
                    throw addError(
2✔
903
                        ifToken,
904
                        "Expected 'end if', 'else if', or 'else' to terminate 'then' block"
905
                    );
906
                }
907

908
                let blockEnd = maybeThenBranch.closingToken;
229✔
909
                if (blockEnd.kind === Lexeme.EndIf) {
229✔
910
                    endIfToken = blockEnd;
152✔
911
                }
912

913
                thenBranch = maybeThenBranch.body;
229✔
914

915
                // attempt to read a bunch of "else if" clauses
916
                while (blockEnd.kind === Lexeme.ElseIf) {
229✔
917
                    elseIfTokens.push(blockEnd);
13✔
918
                    let elseIfCondition = expression();
13✔
919
                    if (checkThen()) {
13✔
920
                        // `then` is optional after `else if ...condition...`, so only advance to the next token if `then` is present
921
                        advance();
8✔
922
                    }
923

924
                    let maybeElseIfThen = block(Lexeme.EndIf, Lexeme.Else, Lexeme.ElseIf);
13✔
925
                    if (!maybeElseIfThen) {
13!
926
                        throw addError(
×
927
                            peek(),
928
                            "Expected 'end if', 'else if', or 'else' to terminate 'then' block"
929
                        );
930
                    }
931

932
                    blockEnd = maybeElseIfThen.closingToken;
13✔
933
                    if (blockEnd.kind === Lexeme.EndIf) {
13✔
934
                        endIfToken = blockEnd;
1✔
935
                    }
936

937
                    elseIfBranches.push({
13✔
938
                        type: "ElseIf",
939
                        condition: elseIfCondition,
940
                        thenBranch: maybeElseIfThen.body,
941
                    });
942
                }
943

944
                if (blockEnd.kind === Lexeme.Else) {
229✔
945
                    elseToken = blockEnd;
76✔
946
                    let maybeElseBranch = block(Lexeme.EndIf);
76✔
947
                    if (!maybeElseBranch) {
76!
948
                        throw addError(peek(), "Expected 'end if' to terminate 'else' block");
×
949
                    }
950
                    elseBranch = maybeElseBranch.body;
76✔
951
                    endIfToken = maybeElseBranch.closingToken;
76✔
952

953
                    //ensure that single-line `if` statements have a colon right before 'end if'
954
                    if (ifToken.location.start.line === endIfToken.location.start.line) {
76✔
955
                        let index = tokens.indexOf(endIfToken);
2✔
956
                        let previousToken = tokens[index - 1];
2✔
957
                        if (previousToken.kind !== Lexeme.Colon) {
2✔
958
                            addError(endIfToken, "Expected ':' to preceed 'end if'");
1✔
959
                        }
960
                    }
961
                    match(Lexeme.Newline);
76✔
962
                } else {
963
                    if (!endIfToken) {
153!
964
                        throw addError(
×
965
                            blockEnd,
966
                            `Expected 'end if' to close 'if' statement started on line ${startingLine.start.line}`
967
                        );
968
                    }
969

970
                    //ensure that single-line `if` statements have a colon right before 'end if'
971
                    if (ifToken.location.start.line === endIfToken.location.start.line) {
153✔
972
                        let index = tokens.indexOf(endIfToken);
4✔
973
                        let previousToken = tokens[index - 1];
4✔
974
                        if (previousToken.kind !== Lexeme.Colon) {
4✔
975
                            addError(endIfToken, "Expected ':' to preceed 'end if'");
1✔
976
                        }
977
                    }
978
                    match(Lexeme.Newline);
153✔
979
                }
980
            } else {
981
                let maybeThenBranch = block(Lexeme.Newline, Lexeme.Eof, Lexeme.ElseIf, Lexeme.Else);
27✔
982
                if (!maybeThenBranch) {
27!
983
                    throw addError(
×
984
                        peek(),
985
                        "Expected a statement to follow 'if ...condition... then'"
986
                    );
987
                }
988
                thenBranch = maybeThenBranch.body;
27✔
989

990
                let closingToken = maybeThenBranch.closingToken;
27✔
991
                while (closingToken.kind === Lexeme.ElseIf) {
27✔
992
                    let elseIf = maybeThenBranch.closingToken;
3✔
993
                    elseIfTokens.push(elseIf);
3✔
994
                    let elseIfCondition = expression();
3✔
995
                    if (checkThen()) {
3✔
996
                        // `then` is optional after `else if ...condition...`, so only advance to the next token if `then` is present
997
                        advance();
2✔
998
                    }
999

1000
                    let maybeElseIfBranch = block(
3✔
1001
                        Lexeme.Newline,
1002
                        Lexeme.Eof,
1003
                        Lexeme.ElseIf,
1004
                        Lexeme.Else
1005
                    );
1006
                    if (!maybeElseIfBranch) {
3!
1007
                        throw addError(
×
1008
                            peek(),
1009
                            `Expected a statement to follow '${elseIf.text} ...condition... then'`
1010
                        );
1011
                    }
1012
                    closingToken = maybeElseIfBranch.closingToken;
3✔
1013

1014
                    elseIfBranches.push({
3✔
1015
                        type: "ElseIf",
1016
                        condition: elseIfCondition,
1017
                        thenBranch: maybeElseIfBranch.body,
1018
                    });
1019
                }
1020

1021
                if (
27✔
1022
                    closingToken.kind !== Lexeme.Newline &&
34!
1023
                    (closingToken.kind === Lexeme.Else || match(Lexeme.Else))
1024
                ) {
1025
                    elseToken = closingToken;
7✔
1026
                    let maybeElseBranch = block(Lexeme.Newline, Lexeme.Eof);
7✔
1027
                    if (!maybeElseBranch) {
7!
1028
                        throw addError(peek(), `Expected a statement to follow 'else'`);
×
1029
                    }
1030
                    elseBranch = maybeElseBranch.body;
7✔
1031
                }
1032
            }
1033

1034
            return new Stmt.If(
256✔
1035
                {
1036
                    if: ifToken,
1037
                    then: thenToken,
1038
                    elseIfs: elseIfTokens,
1039
                    endIf: endIfToken,
1040
                    else: elseToken,
1041
                },
1042
                condition,
1043
                thenBranch,
1044
                elseIfBranches,
1045
                elseBranch
1046
            );
1047
        }
1048

1049
        function setStatement(
1050
            ...additionalTerminators: BlockTerminator[]
1051
        ): Stmt.DottedSet | Stmt.IndexedSet | Stmt.Expression | Stmt.Increment {
1052
            /**
1053
             * Attempts to find an expression-statement or an increment statement.
1054
             * While calls are valid expressions _and_ statements, increment (e.g. `foo++`)
1055
             * statements aren't valid expressions. They _do_ however fall under the same parsing
1056
             * priority as standalone function calls though, so we cann parse them in the same way.
1057
             */
1058
            function _expressionStatement(): Stmt.Expression | Stmt.Increment {
1059
                let expressionStart = peek();
4,908✔
1060

1061
                if (check(Lexeme.PlusPlus, Lexeme.MinusMinus)) {
4,908✔
1062
                    let operator = advance();
21✔
1063

1064
                    if (check(Lexeme.PlusPlus, Lexeme.MinusMinus)) {
21✔
1065
                        throw addError(
1✔
1066
                            peek(),
1067
                            "Consecutive increment/decrement operators are not allowed"
1068
                        );
1069
                    } else if (expr instanceof Expr.Call) {
20✔
1070
                        throw addError(
1✔
1071
                            expressionStart,
1072
                            "Increment/decrement operators are not allowed on the result of a function call"
1073
                        );
1074
                    }
1075

1076
                    return new Stmt.Increment(expr, operator);
19✔
1077
                }
1078

1079
                if (!check(...additionalTerminators)) {
4,887✔
1080
                    consume(
4,886✔
1081
                        "Expected newline or ':' after expression statement",
1082
                        Lexeme.Newline,
1083
                        Lexeme.Colon,
1084
                        Lexeme.Eof
1085
                    );
1086
                }
1087

1088
                if (expr instanceof Expr.Call) {
4,887✔
1089
                    return new Stmt.Expression(expr);
4,884✔
1090
                }
1091

1092
                throw addError(
3✔
1093
                    expressionStart,
1094
                    "Expected statement or function call, but received an expression"
1095
                );
1096
            }
1097

1098
            let expr = call();
7,801✔
1099
            if (check(...assignmentOperators) && !(expr instanceof Expr.Call)) {
7,794✔
1100
                let left = expr;
2,886✔
1101
                let operator = advance();
2,886✔
1102
                let right = expression();
2,886✔
1103

1104
                // Create a dotted or indexed "set" based on the left-hand side's type
1105
                if (left instanceof Expr.IndexedGet) {
2,886✔
1106
                    consume(
69✔
1107
                        "Expected newline or ':' after indexed 'set' statement",
1108
                        Lexeme.Newline,
1109
                        Lexeme.Else,
1110
                        Lexeme.ElseIf,
1111
                        Lexeme.Colon,
1112
                        Lexeme.Eof
1113
                    );
1114

1115
                    return new Stmt.IndexedSet(
69✔
1116
                        left.obj,
1117
                        left.indexes,
1118
                        operator.kind === Lexeme.Equal
69✔
1119
                            ? right
1120
                            : new Expr.Binary(left, operator, right),
1121
                        left.closingSquare
1122
                    );
1123
                } else if (left instanceof Expr.DottedGet) {
2,817!
1124
                    consume(
2,817✔
1125
                        "Expected newline or ':' after dotted 'set' statement",
1126
                        Lexeme.Newline,
1127
                        Lexeme.Else,
1128
                        Lexeme.ElseIf,
1129
                        Lexeme.Colon,
1130
                        Lexeme.Eof
1131
                    );
1132

1133
                    return new Stmt.DottedSet(
2,817✔
1134
                        left.obj,
1135
                        left.name,
1136
                        operator.kind === Lexeme.Equal
2,817✔
1137
                            ? right
1138
                            : new Expr.Binary(left, operator, right)
1139
                    );
1140
                } else {
1141
                    return _expressionStatement();
×
1142
                }
1143
            } else {
1144
                return _expressionStatement();
4,908✔
1145
            }
1146
        }
1147

1148
        function printStatement(...additionalterminators: BlockTerminator[]): Stmt.Print {
1149
            let printKeyword = advance();
9,996✔
1150

1151
            let values: (Expr.Expression | Stmt.PrintSeparator.Tab | Stmt.PrintSeparator.Space)[] =
1152
                [];
9,996✔
1153

1154
            //print statements can be empty, so look for empty print conditions
1155
            if (isAtEnd() || check(Lexeme.Newline, Lexeme.Colon)) {
9,996✔
1156
                let emptyStringLiteral = new Expr.Literal(new BrsString(""), printKeyword.location);
1✔
1157
                values.push(emptyStringLiteral);
1✔
1158
            } else {
1159
                values.push(expression());
9,995✔
1160
            }
1161

1162
            while (!check(Lexeme.Newline, Lexeme.Colon, ...additionalterminators) && !isAtEnd()) {
9,996✔
1163
                if (check(Lexeme.Semicolon)) {
4,884✔
1164
                    values.push(advance() as Stmt.PrintSeparator.Space);
26✔
1165
                }
1166

1167
                if (check(Lexeme.Comma)) {
4,884✔
1168
                    values.push(advance() as Stmt.PrintSeparator.Tab);
3✔
1169
                }
1170

1171
                if (!check(Lexeme.Newline, Lexeme.Colon) && !isAtEnd()) {
4,884✔
1172
                    values.push(expression());
4,883✔
1173
                }
1174
            }
1175

1176
            if (!check(...additionalterminators)) {
9,996✔
1177
                consume(
9,987✔
1178
                    "Expected newline or ':' after printed values",
1179
                    Lexeme.Newline,
1180
                    Lexeme.Colon,
1181
                    Lexeme.Eof
1182
                );
1183
            }
1184

1185
            return new Stmt.Print({ print: printKeyword }, values);
9,996✔
1186
        }
1187

1188
        /**
1189
         * Parses a return statement with an optional return value.
1190
         * @returns an AST representation of a return statement.
1191
         */
1192
        function returnStatement(): Stmt.Return {
1193
            let tokens = { return: previous() };
1,176✔
1194

1195
            if (check(Lexeme.Colon, Lexeme.Newline, Lexeme.Eof)) {
1,176✔
1196
                while (match(Lexeme.Colon, Lexeme.Newline, Lexeme.Eof));
65✔
1197
                return new Stmt.Return(tokens);
65✔
1198
            }
1199

1200
            let toReturn = expression();
1,111✔
1201
            while (match(Lexeme.Newline, Lexeme.Colon));
1,111✔
1202

1203
            return new Stmt.Return(tokens, toReturn);
1,111✔
1204
        }
1205
        /**
1206
         * Parses a `throw` statement with an error value.
1207
         * @returns an AST representation of a throw statement.
1208
         */
1209
        function throwStatement(): Stmt.Throw {
1210
            let tokens = { throw: previous() };
2✔
1211

1212
            let toThrow = expression();
2✔
1213
            while (match(Lexeme.Newline, Lexeme.Colon));
2✔
1214

1215
            return new Stmt.Throw(tokens, toThrow);
2✔
1216
        }
1217

1218
        /**
1219
         * Parses a `label` statement
1220
         * @returns an AST representation of an `label` statement.
1221
         */
1222
        function labelStatement() {
1223
            let tokens = {
2✔
1224
                identifier: advance(),
1225
                colon: advance(),
1226
            };
1227

1228
            consume("Labels must be declared on their own line", Lexeme.Newline, Lexeme.Eof);
2✔
1229

1230
            return new Stmt.Label(tokens);
2✔
1231
        }
1232

1233
        /**
1234
         * Parses a `dim` statement
1235
         * @returns an AST representation of an `goto` statement.
1236
         */
1237
        function dimStatement() {
1238
            let dimToken = advance();
3✔
1239

1240
            let name = consume("Expected variable name after 'dim'", Lexeme.Identifier);
3✔
1241

1242
            match(Lexeme.LeftSquare);
3✔
1243

1244
            let dimensions: Expression[] = [expression()];
3✔
1245
            while (!match(Lexeme.RightSquare)) {
2✔
1246
                consume("Expected ',' after expression in 'dim' statement", Lexeme.Comma);
1✔
1247
                dimensions.push(expression());
1✔
1248
            }
1249
            let rightSquare = previous();
2✔
1250

1251
            let tokens = {
2✔
1252
                dim: dimToken,
1253
                closingBrace: rightSquare,
1254
            };
1255

1256
            return new Stmt.Dim(tokens, name as Identifier, dimensions);
2✔
1257
        }
1258

1259
        /**
1260
         * Parses a `goto` statement
1261
         * @returns an AST representation of an `goto` statement.
1262
         */
1263
        function gotoStatement() {
1264
            let tokens = {
3✔
1265
                goto: advance(),
1266
                label: consume("Expected label identifier after goto keyword", Lexeme.Identifier),
1267
            };
1268

1269
            while (match(Lexeme.Newline, Lexeme.Colon));
3✔
1270

1271
            return new Stmt.Goto(tokens);
3✔
1272
        }
1273

1274
        /**
1275
         * Parses an `end` statement
1276
         * @returns an AST representation of an `end` statement.
1277
         */
1278
        function endStatement() {
1279
            let tokens = { end: advance() };
1✔
1280

1281
            while (match(Lexeme.Newline));
1✔
1282

1283
            return new Stmt.End(tokens);
1✔
1284
        }
1285
        /**
1286
         * Parses a `stop` statement
1287
         * @returns an AST representation of a `stop` statement
1288
         */
1289
        function stopStatement() {
1290
            let tokens = { stop: advance() };
2✔
1291

1292
            while (match(Lexeme.Newline, Lexeme.Colon));
2✔
1293

1294
            return new Stmt.Stop(tokens);
2✔
1295
        }
1296

1297
        /**
1298
         * Parses a block, looking for a specific terminating Lexeme to denote completion.
1299
         * @param terminators the token(s) that signifies the end of this block; all other terminators are
1300
         *                    ignored.
1301
         */
1302
        function block(
1303
            ...terminators: BlockTerminator[]
1304
        ): { body: Stmt.Block; closingToken: Token } | undefined {
1305
            let startingToken = peek();
4,397✔
1306

1307
            let statementSeparators = [Lexeme.Colon];
4,397✔
1308
            if (!terminators.includes(Lexeme.Newline)) {
4,397✔
1309
                statementSeparators.push(Lexeme.Newline);
4,360✔
1310
            }
1311

1312
            while (match(...statementSeparators));
4,397✔
1313

1314
            let closingToken: Token | undefined;
1315
            const statements: Statement[] = [];
4,397✔
1316
            while (!check(...terminators) && !isAtEnd()) {
4,397✔
1317
                //grab the location of the current token
1318
                let loopCurrent = current;
22,304✔
1319
                let dec = declaration(...terminators);
22,304✔
1320

1321
                if (dec) {
22,304✔
1322
                    statements.push(dec);
22,297✔
1323
                } else {
1324
                    //something went wrong. reset to the top of the loop
1325
                    current = loopCurrent;
7✔
1326

1327
                    //scrap the entire line
1328
                    consumeUntil(Lexeme.Colon, Lexeme.Newline, Lexeme.Eof);
7✔
1329
                    //trash the newline character so we start the next iteraion on the next line
1330
                    advance();
7✔
1331
                }
1332

1333
                if (checkPrevious(...terminators)) {
22,304✔
1334
                    closingToken = previous();
13✔
1335
                    while (match(...statementSeparators));
13✔
1336
                    break;
13✔
1337
                } else {
1338
                    while (match(...statementSeparators));
22,291✔
1339
                }
1340
            }
1341

1342
            if (isAtEnd() && !terminators.includes(Lexeme.Eof)) {
4,397✔
1343
                return undefined;
4✔
1344
                // TODO: Figure out how to handle unterminated blocks well
1345
            }
1346

1347
            // consume the last terminator
1348
            if (check(...terminators) && !closingToken) {
4,393✔
1349
                closingToken = advance();
4,380✔
1350
            }
1351

1352
            if (!closingToken) {
4,393!
1353
                return undefined;
×
1354
            }
1355

1356
            //the block's location starts at the end of the preceding token, and stops at the beginning of the `end` token
1357
            const location: Location = {
4,393✔
1358
                file: startingToken.location.file,
1359
                start: startingToken.location.start,
1360
                end: closingToken.location.start,
1361
            };
1362

1363
            return {
4,393✔
1364
                body: new Stmt.Block(statements, location),
1365
                closingToken,
1366
            };
1367
        }
1368

1369
        function expression(): Expression {
1370
            return anonymousFunction();
39,879✔
1371
        }
1372

1373
        function anonymousFunction(): Expression {
1374
            if (check(Lexeme.Sub, Lexeme.Function)) {
39,879✔
1375
                return functionDeclaration(true);
240✔
1376
            }
1377

1378
            return boolean();
39,639✔
1379
        }
1380

1381
        function boolean(): Expression {
1382
            let expr = relational();
39,639✔
1383

1384
            while (match(Lexeme.And, Lexeme.Or)) {
39,634✔
1385
                let operator = previous();
24✔
1386
                let right = relational();
24✔
1387
                expr = new Expr.Binary(expr, operator, right);
24✔
1388
            }
1389

1390
            return expr;
39,634✔
1391
        }
1392

1393
        function relational(): Expression {
1394
            let expr = bitshift();
39,747✔
1395

1396
            while (
39,742✔
1397
                match(
1398
                    Lexeme.Equal,
1399
                    Lexeme.LessGreater,
1400
                    Lexeme.Greater,
1401
                    Lexeme.GreaterEqual,
1402
                    Lexeme.Less,
1403
                    Lexeme.LessEqual
1404
                )
1405
            ) {
1406
                let operator = previous();
547✔
1407
                let right = bitshift();
547✔
1408
                expr = new Expr.Binary(expr, operator, right);
547✔
1409
            }
1410

1411
            return expr;
39,742✔
1412
        }
1413

1414
        function bitshift(): Expression {
1415
            let expr = additive();
40,294✔
1416

1417
            while (match(Lexeme.LeftShift, Lexeme.RightShift)) {
40,289✔
1418
                let operator = previous();
6✔
1419
                let right = additive();
6✔
1420
                expr = new Expr.Binary(expr, operator, right);
6✔
1421
            }
1422

1423
            return expr;
40,289✔
1424
        }
1425

1426
        function additive(): Expression {
1427
            let expr = multiplicative();
40,300✔
1428

1429
            while (match(Lexeme.Plus, Lexeme.Minus)) {
40,295✔
1430
                let operator = previous();
785✔
1431
                let right = multiplicative();
785✔
1432
                expr = new Expr.Binary(expr, operator, right);
785✔
1433
            }
1434

1435
            return expr;
40,295✔
1436
        }
1437

1438
        function multiplicative(): Expression {
1439
            let expr = exponential();
41,085✔
1440

1441
            while (match(Lexeme.Slash, Lexeme.Backslash, Lexeme.Star, Lexeme.Mod)) {
41,080✔
1442
                let operator = previous();
39✔
1443
                let right = exponential();
39✔
1444
                expr = new Expr.Binary(expr, operator, right);
39✔
1445
            }
1446

1447
            return expr;
41,080✔
1448
        }
1449

1450
        function exponential(): Expression {
1451
            let expr = prefixUnary();
41,124✔
1452

1453
            while (match(Lexeme.Caret)) {
41,119✔
1454
                let operator = previous();
9✔
1455
                let right = prefixUnary();
9✔
1456
                expr = new Expr.Binary(expr, operator, right);
9✔
1457
            }
1458

1459
            return expr;
41,119✔
1460
        }
1461

1462
        function prefixUnary(): Expression {
1463
            if (match(Lexeme.Not)) {
41,304✔
1464
                let operator = previous();
84✔
1465
                let right = relational();
84✔
1466
                return new Expr.Unary(operator, right);
84✔
1467
            } else if (match(Lexeme.Minus, Lexeme.Plus)) {
41,220✔
1468
                let operator = previous();
171✔
1469
                let right = prefixUnary();
171✔
1470
                return new Expr.Unary(operator, right);
171✔
1471
            }
1472

1473
            return call();
41,049✔
1474
        }
1475

1476
        function call(): Expression {
1477
            let expr = primary();
48,850✔
1478

1479
            function indexedGet() {
1480
                let elements: Expression[] = [];
450✔
1481

1482
                while (match(Lexeme.Newline));
450✔
1483

1484
                if (!match(Lexeme.RightSquare)) {
450✔
1485
                    elements.push(expression());
450✔
1486

1487
                    while (match(Lexeme.Comma, Lexeme.Newline)) {
450✔
1488
                        while (match(Lexeme.Newline));
9✔
1489

1490
                        if (check(Lexeme.RightSquare)) {
9!
NEW
1491
                            break;
×
1492
                        }
1493

1494
                        elements.push(expression());
9✔
1495
                    }
1496
                }
1497
                if (elements.length === 0) {
450!
NEW
1498
                    throw addError(peek(), "Expected expression inside brackets []");
×
1499
                }
1500
                let closingSquare = consume(
450✔
1501
                    "Expected ']' after array or object index",
1502
                    Lexeme.RightSquare
1503
                );
1504

1505
                expr = new Expr.IndexedGet(expr, elements, closingSquare);
450✔
1506
            }
1507

1508
            function dottedGet() {}
1509

1510
            while (true) {
48,840✔
1511
                if (match(Lexeme.LeftParen)) {
83,361✔
1512
                    expr = finishCall(expr);
13,801✔
1513
                } else if (match(Lexeme.LeftSquare)) {
69,560✔
1514
                    indexedGet();
447✔
1515
                } else if (match(Lexeme.Dot)) {
69,113✔
1516
                    if (match(Lexeme.LeftSquare)) {
20,275✔
1517
                        indexedGet();
3✔
1518
                    } else {
1519
                        while (match(Lexeme.Newline));
20,272✔
1520

1521
                        let name = consume(
20,272✔
1522
                            "Expected property name after '.'",
1523
                            Lexeme.Identifier,
1524
                            ...allowedProperties
1525
                        );
1526

1527
                        // force it into an identifier so the AST makes some sense
1528
                        name.kind = Lexeme.Identifier;
20,271✔
1529

1530
                        expr = new Expr.DottedGet(expr, name as Identifier);
20,271✔
1531
                    }
1532
                } else {
1533
                    break;
48,838✔
1534
                }
1535
            }
1536

1537
            return expr;
48,838✔
1538
        }
1539

1540
        function finishCall(callee: Expression): Expression {
1541
            let args = [];
13,801✔
1542
            while (match(Lexeme.Newline));
13,801✔
1543

1544
            if (!check(Lexeme.RightParen)) {
13,801✔
1545
                do {
9,373✔
1546
                    while (match(Lexeme.Newline));
13,511✔
1547

1548
                    if (args.length >= Expr.Call.MaximumArguments) {
13,511!
1549
                        throw addError(
×
1550
                            peek(),
1551
                            `Cannot have more than ${Expr.Call.MaximumArguments} arguments`
1552
                        );
1553
                    }
1554
                    args.push(expression());
13,511✔
1555
                } while (match(Lexeme.Comma));
1556
            }
1557

1558
            while (match(Lexeme.Newline));
13,800✔
1559
            const closingParen = consume(
13,800✔
1560
                "Expected ')' after function call arguments",
1561
                Lexeme.RightParen
1562
            );
1563

1564
            return new Expr.Call(callee, closingParen, args);
13,800✔
1565
        }
1566

1567
        function primary(): Expression {
1568
            switch (true) {
48,850!
1569
                case match(Lexeme.False):
1570
                    return new Expr.Literal(BrsBoolean.False, previous().location);
973✔
1571
                case match(Lexeme.True):
1572
                    return new Expr.Literal(BrsBoolean.True, previous().location);
1,150✔
1573
                case match(Lexeme.Invalid):
1574
                    return new Expr.Literal(BrsInvalid.Instance, previous().location);
144✔
1575
                case match(
1576
                    Lexeme.Integer,
1577
                    Lexeme.LongInteger,
1578
                    Lexeme.Float,
1579
                    Lexeme.Double,
1580
                    Lexeme.String
1581
                ):
1582
                    return new Expr.Literal(previous().literal!, previous().location);
21,717✔
1583
                case match(Lexeme.Identifier):
1584
                    return new Expr.Variable(previous() as Identifier);
22,926✔
1585
                case match(Lexeme.LeftParen):
1586
                    let left = previous();
186✔
1587
                    let expr = expression();
186✔
1588
                    let right = consume(
186✔
1589
                        "Unmatched '(' - expected ')' after expression",
1590
                        Lexeme.RightParen
1591
                    );
1592
                    return new Expr.Grouping({ left, right }, expr);
186✔
1593
                case match(Lexeme.LeftSquare):
1594
                    let elements: Expression[] = [];
561✔
1595
                    let openingSquare = previous();
561✔
1596

1597
                    while (match(Lexeme.Newline));
561✔
1598

1599
                    if (!match(Lexeme.RightSquare)) {
561✔
1600
                        elements.push(expression());
556✔
1601

1602
                        while (match(Lexeme.Comma, Lexeme.Newline)) {
556✔
1603
                            while (match(Lexeme.Newline));
886✔
1604

1605
                            if (check(Lexeme.RightSquare)) {
886✔
1606
                                break;
3✔
1607
                            }
1608

1609
                            elements.push(expression());
883✔
1610
                        }
1611

1612
                        consume(
556✔
1613
                            "Unmatched '[' - expected ']' after array literal",
1614
                            Lexeme.RightSquare
1615
                        );
1616
                    }
1617

1618
                    let closingSquare = previous();
561✔
1619

1620
                    //consume("Expected newline or ':' after array literal", Lexeme.Newline, Lexeme.Colon, Lexeme.Eof);
1621
                    return new Expr.ArrayLiteral(elements, openingSquare, closingSquare);
561✔
1622
                case match(Lexeme.LeftBrace):
1623
                    let openingBrace = previous();
1,183✔
1624
                    let members: Expr.AAMember[] = [];
1,183✔
1625

1626
                    function key() {
1627
                        let k;
1628
                        if (check(Lexeme.Identifier, ...allowedProperties)) {
1,621✔
1629
                            k = new BrsString(advance().text!);
1,541✔
1630
                        } else if (check(Lexeme.String)) {
80!
1631
                            k = advance().literal! as BrsString;
80✔
1632
                        } else {
1633
                            throw addError(
×
1634
                                peek(),
1635
                                `Expected identifier or string as associative array key, but received '${
1636
                                    peek().text || ""
×
1637
                                }'`
1638
                            );
1639
                        }
1640

1641
                        consume(
1,621✔
1642
                            "Expected ':' between associative array key and value",
1643
                            Lexeme.Colon
1644
                        );
1645
                        return k;
1,621✔
1646
                    }
1647

1648
                    while (match(Lexeme.Newline));
1,183✔
1649

1650
                    if (!match(Lexeme.RightBrace)) {
1,183✔
1651
                        members.push({
938✔
1652
                            name: key(),
1653
                            value: expression(),
1654
                        });
1655

1656
                        while (match(Lexeme.Comma, Lexeme.Newline, Lexeme.Colon)) {
938✔
1657
                            while (match(Lexeme.Newline, Lexeme.Colon));
1,193✔
1658

1659
                            if (check(Lexeme.RightBrace)) {
1,193✔
1660
                                break;
510✔
1661
                            }
1662

1663
                            members.push({
683✔
1664
                                name: key(),
1665
                                value: expression(),
1666
                            });
1667
                        }
1668

1669
                        consume(
938✔
1670
                            "Unmatched '{' - expected '}' after associative array literal",
1671
                            Lexeme.RightBrace
1672
                        );
1673
                    }
1674

1675
                    let closingBrace = previous();
1,183✔
1676

1677
                    return new Expr.AALiteral(members, openingBrace, closingBrace);
1,183✔
1678
                case match(Lexeme.Pos, Lexeme.Tab):
1679
                    let token = Object.assign(previous(), {
×
1680
                        kind: Lexeme.Identifier,
1681
                    }) as Identifier;
1682
                    return new Expr.Variable(token);
×
1683
                case check(Lexeme.Function, Lexeme.Sub):
1684
                    return anonymousFunction();
×
1685
                default:
1686
                    throw addError(peek(), `Found unexpected token '${peek().text}'`);
10✔
1687
            }
1688
        }
1689

1690
        function match(...lexemes: Lexeme[]) {
1691
            for (let lexeme of lexemes) {
987,652✔
1692
                if (check(lexeme)) {
1,736,633✔
1693
                    advance();
103,732✔
1694
                    return true;
103,732✔
1695
                }
1696
            }
1697

1698
            return false;
883,920✔
1699
        }
1700

1701
        /**
1702
         * Consume tokens until one of the `stopLexemes` is encountered
1703
         * @param lexemes
1704
         * @return - the list of tokens consumed, EXCLUDING the `stopLexeme` (you can use `peek()` to see which one it was)
1705
         */
1706
        function consumeUntil(...stopLexemes: Lexeme[]) {
1707
            let result = [] as Token[];
14✔
1708
            //take tokens until we encounter one of the stopLexemes
1709
            while (!stopLexemes.includes(peek().kind)) {
14✔
1710
                result.push(advance());
12✔
1711
            }
1712
            return result;
14✔
1713
        }
1714

1715
        /**
1716
         * Checks that the next token is one of a list of lexemes and returns that token, and *advances past it*.
1717
         * If the next token is none of the provided lexemes, throws an error.
1718
         * @param message - the message to include in the thrown error if the next token isn't one of the provided `lexemes`
1719
         * @param lexemes - the set of `lexemes` to check for
1720
         *
1721
         * @see checkOrError
1722
         */
1723
        function consume(message: string, ...lexemes: Lexeme[]): Token {
1724
            let foundLexeme = lexemes
69,773✔
1725
                .map((lexeme) => peek().kind === lexeme)
1,059,417✔
1726
                .reduce((foundAny, foundCurrent) => foundAny || foundCurrent, false);
1,059,417✔
1727

1728
            if (foundLexeme) {
69,773✔
1729
                return advance();
69,772✔
1730
            }
1731
            throw addError(peek(), message);
1✔
1732
        }
1733

1734
        function advance(): Token {
1735
            if (!isAtEnd()) {
209,581✔
1736
                current++;
209,462✔
1737
            }
1738
            return previous();
209,581✔
1739
        }
1740

1741
        /**
1742
         * Checks that the next token is one of a list of lexemes and returns that token, but *does not advance past it*.
1743
         * If the next token is none of the provided lexemes, throws an error.
1744
         * @param message - the message to include in the thrown error if the next token isn't one of the provided `lexemes`
1745
         * @param lexemes - the set of `lexemes` to check for
1746
         *
1747
         * @see consume
1748
         */
1749
        function checkOrThrow(message: string, ...lexemes: Lexeme[]): Token {
1750
            let foundLexeme = lexemes
3,943✔
1751
                .map((lexeme) => peek().kind === lexeme)
7,875✔
1752
                .reduce((foundAny, foundCurrent) => foundAny || foundCurrent, false);
7,875✔
1753
            if (foundLexeme) {
3,943✔
1754
                return peek();
3,943✔
1755
            }
1756

1757
            throw addError(peek(), message);
×
1758
        }
1759

1760
        /**
1761
         * Check that the previous token matches one of the specified Lexemes
1762
         * @param lexemes
1763
         */
1764
        function checkPrevious(...lexemes: Lexeme[]) {
1765
            if (current === 0) {
22,304!
1766
                return false;
×
1767
            } else {
1768
                current--;
22,304✔
1769
                var result = check(...lexemes);
22,304✔
1770
                current++;
22,304✔
1771
                return result;
22,304✔
1772
            }
1773
        }
1774

1775
        function check(...lexemes: Lexeme[]) {
1776
            if (isAtEnd()) {
2,176,501✔
1777
                return false;
6,867✔
1778
            }
1779

1780
            return lexemes.some((lexeme) => peek().kind === lexeme);
2,409,856✔
1781
        }
1782

1783
        function checkNext(...lexemes: Lexeme[]) {
1784
            if (isAtEnd()) {
18,971!
1785
                return false;
×
1786
            }
1787

1788
            return lexemes.some((lexeme) => peekNext().kind === lexeme);
74,201✔
1789
        }
1790

1791
        function isAtEnd() {
1792
            return peek().kind === Lexeme.Eof;
2,557,875✔
1793
        }
1794

1795
        function peekNext() {
1796
            if (isAtEnd()) {
74,201!
1797
                return peek();
×
1798
            }
1799
            return tokens[current + 1];
74,201✔
1800
        }
1801

1802
        function peek() {
1803
            return tokens[current];
6,084,566✔
1804
        }
1805

1806
        function previous() {
1807
            return tokens[current - 1];
284,783✔
1808
        }
1809

1810
        function synchronize() {
1811
            advance(); // skip the erroneous token
21✔
1812

1813
            while (!isAtEnd()) {
21✔
1814
                if (previous().kind === Lexeme.Newline || previous().kind === Lexeme.Colon) {
23✔
1815
                    // newlines and ':' characters separate statements
1816
                    return;
3✔
1817
                }
1818

1819
                switch (peek().kind) {
20!
1820
                    case Lexeme.Function:
1821
                    case Lexeme.Sub:
1822
                    case Lexeme.If:
1823
                    case Lexeme.For:
1824
                    case Lexeme.ForEach:
1825
                    case Lexeme.While:
1826
                    case Lexeme.Print:
1827
                    case Lexeme.Return:
1828
                        // start parsing again from the next block starter or obvious
1829
                        // expression start
1830
                        return;
×
1831
                }
1832

1833
                advance();
20✔
1834
            }
1835
        }
1836
    }
1837
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc