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

rokucommunity / brs / #294

24 Apr 2023 07:40PM UTC coverage: 91.705% (+5.4%) from 86.275%
#294

push

web-flow
Merge pull request #1 from rokucommunity/adoption

refactor in preparation for adoption the project

1736 of 2013 branches covered (86.24%)

Branch coverage included in aggregate %.

5152 of 5498 relevant lines covered (93.71%)

8882.84 hits per line

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

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

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

10
import {
138✔
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

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

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

92
/** List of Lexeme that are allowed as local var identifiers. */
93
const allowedIdentifiers = [Lexeme.EndFor, Lexeme.ExitFor, Lexeme.ForEach];
138✔
94

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

143
/** The results of a Parser's parsing pass. */
144
interface ParseResults {
145
    /** The statements produced by the parser. */
146
    statements: Stmt.Statement[];
147
    /** The errors encountered by the Parser. */
148
    errors: ParseError[];
149
}
150

151
export class Parser {
138✔
152
    /** Allows consumers to observe errors as they're detected. */
153
    readonly events = new EventEmitter();
1,638✔
154

155
    /**
156
     * A convenience function, equivalent to `new Parser().parse(toParse)`, that parses an array of
157
     * `Token`s into an abstract syntax tree that can be executed with the `Interpreter`.
158
     * @param toParse the array of tokens to parse
159
     * @returns an array of `Statement` objects that together form the abstract syntax tree of the
160
     *          program
161
     */
162
    static parse(toParse: ReadonlyArray<Token>) {
163
        return new Parser().parse(toParse);
24✔
164
    }
165

166
    /**
167
     * Convenience function to subscribe to the `err` events emitted by `parser.events`.
168
     * @param errorHandler the function to call for every Parser error emitted after subscribing
169
     * @returns an object with a `dispose` function, used to unsubscribe from errors
170
     */
171
    public onError(errorHandler: (err: ParseError) => void) {
172
        this.events.on("err", errorHandler);
1,433✔
173
        return {
1,433✔
174
            dispose: () => {
175
                this.events.removeListener("err", errorHandler);
×
176
            },
177
        };
178
    }
179

180
    /**
181
     * Convenience function to subscribe to a single `err` event emitted by `parser.events`.
182
     * @param errorHandler the function to call for the first Parser error emitted after subscribing
183
     */
184
    public onErrorOnce(errorHandler: (err: ParseError) => void) {
185
        this.events.once("err", errorHandler);
×
186
    }
187

188
    /**
189
     * Parses an array of `Token`s into an abstract syntax tree that can be executed with the `Interpreter`.
190
     * @param toParse the array of tokens to parse
191
     * @returns an array of `Statement` objects that together form the abstract syntax tree of the
192
     *          program
193
     */
194
    parse(toParse: ReadonlyArray<Token>): ParseResults {
195
        let current = 0;
1,624✔
196
        let tokens = toParse;
1,624✔
197

198
        //the depth of the calls to function declarations. Helps some checks know if they are at the root or not.
199
        let functionDeclarationLevel = 0;
1,624✔
200

201
        function isAtRootLevel() {
202
            return functionDeclarationLevel === 0;
39,287✔
203
        }
204

205
        let statements: Statement[] = [];
1,624✔
206

207
        let errors: ParseError[] = [];
1,624✔
208

209
        /**
210
         * Add an error to the parse results.
211
         * @param token - the token where the error occurred
212
         * @param message - the message for this error
213
         * @returns an error object that can be thrown if the calling code needs to abort parsing
214
         */
215
        const addError = (token: Token, message: string) => {
1,624✔
216
            let err = new ParseError(token, message);
34✔
217
            errors.push(err);
34✔
218
            this.events.emit("err", err);
34✔
219
            return err;
34✔
220
        };
221

222
        /**
223
         * Add an error at the given location.
224
         * @param location
225
         * @param message
226
         */
227
        const addErrorAtLocation = (location: Location, message: string) => {
1,624✔
228
            addError({ location: location } as any, message);
6✔
229
        };
230

231
        if (toParse.length === 0) {
1,624!
232
            return {
×
233
                statements: [],
234
                errors: [],
235
            };
236
        }
237

238
        try {
1,624✔
239
            while (!isAtEnd()) {
1,624✔
240
                let dec = declaration();
3,831✔
241
                if (dec) {
3,831✔
242
                    statements.push(dec);
3,821✔
243
                }
244
            }
245

246
            return { statements, errors };
1,623✔
247
        } catch (parseError) {
248
            return {
1✔
249
                statements: [],
250
                errors: errors,
251
            };
252
        }
253

254
        /**
255
         * A simple wrapper around `check` to make tests for a `end` identifier.
256
         * `end` is a keyword, but not reserved, so associative arrays can have properties
257
         * called `end`; the parser takes on this task.
258
         * @returns `true` if the next token is an identifier with text `end`, otherwise `false`
259
         */
260
        function checkEnd() {
261
            return check(Lexeme.Identifier) && peek().text.toLowerCase() === "end";
8,451✔
262
        }
263

264
        function declaration(...additionalTerminators: BlockTerminator[]): Statement | undefined {
265
            try {
24,657✔
266
                let statementSeparators = [Lexeme.Colon];
24,657✔
267

268
                if (additionalTerminators.includes(Lexeme.Newline)) {
24,657✔
269
                    // if this declaration can be terminated with a newline, check for one
270
                    // before other statement separators
271
                    if (check(Lexeme.Newline)) {
41!
272
                        return;
×
273
                    }
274
                } else {
275
                    statementSeparators.push(Lexeme.Newline);
24,616✔
276
                }
277

278
                while (match(...statementSeparators));
24,657✔
279

280
                if (additionalTerminators.includes(Lexeme.Newline)) {
24,657✔
281
                    // if this declaration can be terminated with a newline, check for one
282
                    // _after_ a series of `:`s.
283
                    if (check(Lexeme.Newline)) {
41!
284
                        return;
×
285
                    }
286
                }
287

288
                // if we reached the end, don't attempt to do anything else
289
                if (isAtEnd()) {
24,657!
290
                    return;
×
291
                }
292

293
                try {
24,657✔
294
                    if (functionDeclarationLevel === 0 && check(Lexeme.Sub, Lexeme.Function)) {
24,657✔
295
                        return functionDeclaration(false);
3,481✔
296
                    }
297

298
                    if (checkLibrary()) {
21,176✔
299
                        return libraryStatement();
7✔
300
                    }
301

302
                    // BrightScript is like python, in that variables can be declared without a `var`,
303
                    // `let`, (...) keyword. As such, we must check the token *after* an identifier to figure
304
                    // out what to do with it.
305
                    if (
21,169✔
306
                        check(Lexeme.Identifier, ...allowedIdentifiers) &&
31,627✔
307
                        checkNext(...assignmentOperators)
308
                    ) {
309
                        return assignment(...additionalTerminators);
3,065✔
310
                    }
311

312
                    return statement(...additionalTerminators);
18,104✔
313
                } finally {
314
                    while (match(...statementSeparators));
24,657✔
315
                }
316
            } catch (error) {
317
                synchronize();
15✔
318
                return;
15✔
319
            }
320
        }
321

322
        function functionDeclaration(isAnonymous: true): Expr.Function;
323
        function functionDeclaration(isAnonymous: false): Stmt.Function;
324
        function functionDeclaration(isAnonymous: boolean) {
325
            try {
3,709✔
326
                //certain statements need to know if they are contained within a function body
327
                //so track the depth here
328
                functionDeclarationLevel++;
3,709✔
329
                let startingKeyword = peek();
3,709✔
330
                let isSub = check(Lexeme.Sub);
3,709✔
331
                let functionType = advance();
3,709✔
332
                let name: Identifier;
333
                let returnType: ValueKind;
334
                let leftParen: Token;
335
                let rightParen: Token;
336

337
                if (isSub) {
3,709✔
338
                    returnType = ValueKind.Void;
2,439✔
339
                } else {
340
                    returnType = ValueKind.Dynamic;
1,270✔
341
                }
342

343
                if (isAnonymous) {
3,709✔
344
                    leftParen = consume(
228✔
345
                        `Expected '(' after ${functionType.text}`,
346
                        Lexeme.LeftParen
347
                    );
348
                } else {
349
                    name = consume(
3,481✔
350
                        `Expected ${functionType.text} name after '${functionType.text}'`,
351
                        Lexeme.Identifier
352
                    ) as Identifier;
353
                    leftParen = consume(
3,481✔
354
                        `Expected '(' after ${functionType.text} name`,
355
                        Lexeme.LeftParen
356
                    );
357

358
                    //prevent functions from ending with type designators
359
                    let lastChar = name.text[name.text.length - 1];
3,481✔
360
                    if (["$", "%", "!", "#", "&"].includes(lastChar)) {
3,481✔
361
                        //don't throw this error; let the parser continue
362
                        addError(
8✔
363
                            name,
364
                            `Function name '${name.text}' cannot end with type designator '${lastChar}'`
365
                        );
366
                    }
367
                }
368

369
                let args: Argument[] = [];
3,709✔
370
                if (!check(Lexeme.RightParen)) {
3,709✔
371
                    do {
991✔
372
                        if (args.length >= Expr.Call.MaximumArguments) {
1,411!
373
                            throw addError(
×
374
                                peek(),
375
                                `Cannot have more than ${Expr.Call.MaximumArguments} arguments`
376
                            );
377
                        }
378

379
                        args.push(signatureArgument());
1,411✔
380
                    } while (match(Lexeme.Comma));
381
                }
382
                rightParen = advance();
3,709✔
383

384
                let maybeAs = peek();
3,709✔
385
                if (check(Lexeme.Identifier) && maybeAs.text.toLowerCase() === "as") {
3,709✔
386
                    advance();
637✔
387

388
                    let typeToken = advance();
637✔
389
                    let typeString = typeToken.text || "";
637!
390
                    let maybeReturnType = ValueKind.fromString(typeString);
637✔
391

392
                    if (!maybeReturnType) {
637!
393
                        throw addError(
×
394
                            typeToken,
395
                            `Function return type '${typeString}' is invalid`
396
                        );
397
                    }
398

399
                    returnType = maybeReturnType;
637✔
400
                }
401

402
                args.reduce((haveFoundOptional: boolean, arg: Argument) => {
3,709✔
403
                    if (haveFoundOptional && !arg.defaultValue) {
1,411!
404
                        throw addError(
×
405
                            {
406
                                kind: Lexeme.Identifier,
407
                                text: arg.name.text,
408
                                isReserved: ReservedWords.has(arg.name.text),
409
                                location: arg.location,
410
                            },
411
                            `Argument '${arg.name.text}' has no default value, but comes after arguments with default values`
412
                        );
413
                    }
414

415
                    return haveFoundOptional || !!arg.defaultValue;
1,411✔
416
                }, false);
417

418
                checkOrThrow(
3,709✔
419
                    `Expected newline or ':' after ${functionType.text} signature`,
420
                    Lexeme.Newline,
421
                    Lexeme.Colon
422
                );
423
                //support ending the function with `end sub` OR `end function`
424
                let maybeBody = block(Lexeme.EndSub, Lexeme.EndFunction);
3,709✔
425
                if (!maybeBody) {
3,709!
426
                    throw addError(
×
427
                        peek(),
428
                        `Expected 'end ${functionType.text}' to terminate ${functionType.text} block`
429
                    );
430
                }
431
                let endingKeyword = maybeBody.closingToken;
3,709✔
432
                let expectedEndKind = isSub ? Lexeme.EndSub : Lexeme.EndFunction;
3,709✔
433

434
                //if `function` is ended with `end sub`, or `sub` is ended with `end function`, then
435
                //add an error but don't hard-fail so the AST can continue more gracefully
436
                if (endingKeyword.kind !== expectedEndKind) {
3,709✔
437
                    addError(
2✔
438
                        endingKeyword,
439
                        `Expected 'end ${functionType.text}' to terminate ${functionType.text} block`
440
                    );
441
                }
442

443
                let func = new Expr.Function(
3,709✔
444
                    args,
445
                    returnType,
446
                    maybeBody.body,
447
                    startingKeyword,
448
                    endingKeyword
449
                );
450

451
                if (isAnonymous) {
3,709✔
452
                    return func;
228✔
453
                } else {
454
                    // only consume trailing newlines in the statement context; expressions
455
                    // expect to handle their own trailing whitespace
456
                    while (match(Lexeme.Newline));
3,481✔
457
                    return new Stmt.Function(name!, func);
3,481✔
458
                }
459
            } finally {
460
                functionDeclarationLevel--;
3,709✔
461
            }
462
        }
463

464
        function signatureArgument(): Argument {
465
            if (!check(Lexeme.Identifier)) {
1,411!
466
                throw addError(
×
467
                    peek(),
468
                    `Expected argument name, but received '${peek().text || ""}'`
×
469
                );
470
            }
471

472
            let name = advance();
1,411✔
473
            let type: ValueKind = ValueKind.Dynamic;
1,411✔
474
            let typeToken: Token | undefined;
475
            let defaultValue;
476

477
            // parse argument default value
478
            if (match(Lexeme.Equal)) {
1,411✔
479
                // it seems any expression is allowed here -- including ones that operate on other arguments!
480
                defaultValue = expression();
79✔
481
            }
482

483
            let next = peek();
1,411✔
484
            if (check(Lexeme.Identifier) && next.text && next.text.toLowerCase() === "as") {
1,411✔
485
                // 'as' isn't a reserved word, so it can't be lexed into an As token without the lexer
486
                // understanding language context.  That's less than ideal, so we'll have to do some
487
                // more intelligent comparisons to detect the 'as' sometimes-keyword here.
488
                advance();
1,105✔
489

490
                typeToken = advance();
1,105✔
491
                let typeValueKind = ValueKind.fromString(typeToken.text);
1,105✔
492

493
                if (!typeValueKind) {
1,105!
494
                    throw addError(
×
495
                        typeToken,
496
                        `Function parameter '${name.text}' is of invalid type '${typeToken.text}'`
497
                    );
498
                }
499

500
                type = typeValueKind;
1,105✔
501
            }
502

503
            return {
1,411✔
504
                name: name,
505
                type: {
506
                    kind: type,
507
                    location: typeToken ? typeToken.location : StdlibArgument.InternalLocation,
1,411✔
508
                },
509
                defaultValue: defaultValue,
510
                location: {
511
                    file: name.location.file,
512
                    start: name.location.start,
513
                    end: typeToken ? typeToken.location.end : name.location.end,
1,411✔
514
                },
515
            };
516
        }
517

518
        function assignment(...additionalterminators: Lexeme[]): Stmt.Assignment {
519
            let name = advance() as Identifier;
3,079✔
520
            //add error if name is a reserved word that cannot be used as an identifier
521
            if (disallowedIdentifiers.has(name.text.toLowerCase())) {
3,079✔
522
                //don't throw...this is fully recoverable
523
                addError(name, `Cannot use reserved word "${name.text}" as an identifier`);
1✔
524
            }
525
            let operator = consume(
3,079✔
526
                `Expected operator ('=', '+=', '-=', '*=', '/=', '\\=', '^=', '<<=', or '>>=') after idenfifier '${name.text}'`,
527
                ...assignmentOperators
528
            );
529

530
            let value = expression();
3,079✔
531
            if (!check(...additionalterminators)) {
3,078✔
532
                consume(
3,053✔
533
                    "Expected newline or ':' after assignment",
534
                    Lexeme.Newline,
535
                    Lexeme.Colon,
536
                    Lexeme.Eof,
537
                    ...additionalterminators
538
                );
539
            }
540

541
            if (operator.kind === Lexeme.Equal) {
3,078✔
542
                return new Stmt.Assignment({ equals: operator }, name, value);
3,066✔
543
            } else {
544
                return new Stmt.Assignment(
12✔
545
                    { equals: operator },
546
                    name,
547
                    new Expr.Binary(new Expr.Variable(name), operator, value)
548
                );
549
            }
550
        }
551

552
        function checkLibrary() {
553
            let isLibraryIdentifier =
554
                check(Lexeme.Identifier) && peek().text.toLowerCase() === "library";
39,280✔
555
            //if we are at the top level, any line that starts with "library" should be considered a library statement
556
            if (isAtRootLevel() && isLibraryIdentifier) {
39,280✔
557
                return true;
6✔
558
            }
559
            //not at root level, library statements are all invalid here, but try to detect if the tokens look
560
            //like a library statement (and let the libraryStatement function handle emitting the errors)
561
            else if (isLibraryIdentifier && checkNext(Lexeme.String)) {
39,274✔
562
                return true;
1✔
563
            }
564
            //definitely not a library statement
565
            else {
566
                return false;
39,273✔
567
            }
568
        }
569

570
        function statement(...additionalterminators: BlockTerminator[]): Statement | undefined {
571
            if (checkLibrary()) {
18,104!
572
                return libraryStatement();
×
573
            }
574

575
            if (check(Lexeme.Stop)) {
18,104✔
576
                return stopStatement();
2✔
577
            }
578

579
            if (check(Lexeme.If)) {
18,102✔
580
                return ifStatement();
241✔
581
            }
582

583
            if (check(Lexeme.Print)) {
17,861✔
584
                return printStatement(...additionalterminators);
9,313✔
585
            }
586

587
            if (check(Lexeme.While)) {
8,548✔
588
                return whileStatement();
11✔
589
            }
590

591
            if (check(Lexeme.ExitWhile)) {
8,537✔
592
                return exitWhile();
2✔
593
            }
594

595
            if (check(Lexeme.For)) {
8,535✔
596
                return forStatement();
14✔
597
            }
598

599
            if (check(Lexeme.ForEach)) {
8,521✔
600
                return forEachStatement();
65✔
601
            }
602

603
            if (check(Lexeme.ExitFor)) {
8,456✔
604
                return exitFor();
5✔
605
            }
606

607
            if (checkEnd()) {
8,451✔
608
                return endStatement();
1✔
609
            }
610

611
            if (match(Lexeme.Return)) {
8,450✔
612
                return returnStatement();
1,118✔
613
            }
614

615
            if (check(Lexeme.Dim)) {
7,332✔
616
                return dimStatement();
3✔
617
            }
618

619
            if (check(Lexeme.Goto)) {
7,329✔
620
                return gotoStatement();
3✔
621
            }
622

623
            //does this line look like a label? (i.e.  `someIdentifier:` )
624
            if (check(Lexeme.Identifier) && checkNext(Lexeme.Colon)) {
7,326✔
625
                return labelStatement();
2✔
626
            }
627

628
            // TODO: support multi-statements
629
            return setStatement(...additionalterminators);
7,324✔
630
        }
631

632
        function whileStatement(): Stmt.While {
633
            const whileKeyword = advance();
11✔
634
            const condition = expression();
11✔
635

636
            checkOrThrow(
11✔
637
                "Expected newline or ':' after 'while ...condition...'",
638
                Lexeme.Newline,
639
                Lexeme.Colon
640
            );
641
            const maybeWhileBlock = block(Lexeme.EndWhile);
11✔
642
            if (!maybeWhileBlock) {
11!
643
                throw addError(peek(), "Expected 'end while' to terminate while-loop block");
×
644
            }
645

646
            return new Stmt.While(
11✔
647
                { while: whileKeyword, endWhile: maybeWhileBlock.closingToken },
648
                condition,
649
                maybeWhileBlock.body
650
            );
651
        }
652

653
        function exitWhile(): Stmt.ExitWhile {
654
            let keyword = advance();
2✔
655
            checkOrThrow("Expected newline after 'exit while'", Lexeme.Newline);
2✔
656
            return new Stmt.ExitWhile({ exitWhile: keyword });
2✔
657
        }
658

659
        function forStatement(): Stmt.For {
660
            const forKeyword = advance();
14✔
661
            const initializer = assignment(Lexeme.To);
14✔
662
            const to = advance();
14✔
663
            const finalValue = expression();
14✔
664
            let increment: Expression | undefined;
665
            let step: Token | undefined;
666

667
            if (check(Lexeme.Step)) {
14✔
668
                step = advance();
6✔
669
                increment = expression();
6✔
670
            } else {
671
                // BrightScript for/to/step loops default to a step of 1 if no `step` is provided
672
                increment = new Expr.Literal(new Int32(1), peek().location);
8✔
673
            }
674

675
            let maybeBody = block(Lexeme.EndFor, Lexeme.Next);
14✔
676
            if (!maybeBody) {
14!
677
                throw addError(peek(), "Expected 'end for' or 'next' to terminate for-loop block");
×
678
            }
679

680
            // WARNING: BrightScript doesn't delete the loop initial value after a for/to loop! It just
681
            // stays around in scope with whatever value it was when the loop exited.
682
            return new Stmt.For(
14✔
683
                {
684
                    for: forKeyword,
685
                    to: to,
686
                    step: step,
687
                    endFor: maybeBody.closingToken,
688
                },
689
                initializer,
690
                finalValue,
691
                increment,
692
                maybeBody.body
693
            );
694
        }
695

696
        function forEachStatement(): Stmt.ForEach {
697
            let forEach = advance();
65✔
698
            let name = advance();
65✔
699

700
            let maybeIn = peek();
65✔
701
            if (check(Lexeme.Identifier) && maybeIn.text.toLowerCase() === "in") {
65!
702
                advance();
65✔
703
            } else {
704
                throw addError(maybeIn, "Expected 'in' after 'for each <name>'");
×
705
            }
706

707
            let target = expression();
65✔
708
            if (!target) {
65!
709
                throw addError(peek(), "Expected target object to iterate over");
×
710
            }
711
            advance();
65✔
712

713
            let maybeBody = block(Lexeme.EndFor, Lexeme.Next);
65✔
714
            if (!maybeBody) {
65!
715
                throw addError(peek(), "Expected 'end for' or 'next' to terminate for-loop block");
×
716
            }
717

718
            return new Stmt.ForEach(
65✔
719
                {
720
                    forEach: forEach,
721
                    in: maybeIn,
722
                    endFor: maybeBody.closingToken,
723
                },
724
                name,
725
                target,
726
                maybeBody.body
727
            );
728
        }
729

730
        function exitFor(): Stmt.ExitFor {
731
            let keyword = advance();
5✔
732
            checkOrThrow("Expected newline after 'exit for'", Lexeme.Newline);
5✔
733
            return new Stmt.ExitFor({ exitFor: keyword });
5✔
734
        }
735

736
        function libraryStatement(): Stmt.Library | undefined {
737
            let libraryStatement = new Stmt.Library({
7✔
738
                library: advance(),
739
                //grab the next token only if it's a string
740
                filePath: check(Lexeme.String) ? advance() : undefined,
7✔
741
            });
742

743
            //no token following library keyword token
744
            if (!libraryStatement.tokens.filePath && check(Lexeme.Newline, Lexeme.Colon)) {
7✔
745
                addErrorAtLocation(
1✔
746
                    libraryStatement.tokens.library.location,
747
                    `Missing string literal after ${libraryStatement.tokens.library.text} keyword`
748
                );
749
            }
750
            //does not have a string literal as next token
751
            else if (!libraryStatement.tokens.filePath && peek().kind === Lexeme.Newline) {
6!
752
                addErrorAtLocation(
×
753
                    peek().location,
754
                    `Expected string literal after ${libraryStatement.tokens.library.text} keyword`
755
                );
756
            }
757

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

761
            if (invalidTokens.length > 0) {
7✔
762
                //add an error for every invalid token
763
                for (let invalidToken of invalidTokens) {
1✔
764
                    addErrorAtLocation(
3✔
765
                        invalidToken.location,
766
                        `Found unexpected token '${invalidToken.text}' after library statement`
767
                    );
768
                }
769
            }
770

771
            //libraries must be at the very top of the file before any other declarations.
772
            let isAtTopOfFile = true;
7✔
773
            for (let statement of statements) {
7✔
774
                //if we found a non-library statement, this statement is not at the top of the file
775
                if (!(statement instanceof Stmt.Library)) {
2✔
776
                    isAtTopOfFile = false;
1✔
777
                }
778
            }
779

780
            //libraries must be a root-level statement (i.e. NOT nested inside of functions)
781
            if (!isAtRootLevel() || !isAtTopOfFile) {
7✔
782
                addErrorAtLocation(
2✔
783
                    libraryStatement.location,
784
                    "Library statements may only appear at the top of a file"
785
                );
786
            }
787
            //consume to the next newline, eof, or colon
788
            while (match(Lexeme.Newline, Lexeme.Eof, Lexeme.Colon));
7✔
789
            return libraryStatement;
7✔
790
        }
791

792
        function ifStatement(): Stmt.If {
793
            const ifToken = advance();
241✔
794
            const startingLine = ifToken.location;
241✔
795

796
            const condition = expression();
241✔
797
            let thenBranch: Stmt.Block;
798
            let elseIfBranches: Stmt.ElseIf[] = [];
239✔
799
            let elseBranch: Stmt.Block | undefined;
800

801
            let thenToken: Token | undefined;
802
            let elseIfTokens: Token[] = [];
239✔
803
            let endIfToken: Token | undefined;
804
            let elseToken: Token | undefined;
805

806
            /**
807
             * A simple wrapper around `check`, to make tests for a `then` identifier.
808
             * As with many other words, "then" is a keyword but not reserved, so associative
809
             * arrays can have properties called "then".  It's a valid identifier sometimes, so the
810
             * parser has to take on the burden of understanding that I guess.
811
             * @returns `true` if the next token is an identifier with text "then", otherwise `false`.
812
             */
813
            function checkThen() {
814
                return check(Lexeme.Identifier) && peek().text.toLowerCase() === "then";
255✔
815
            }
816

817
            if (checkThen()) {
239✔
818
                // `then` is optional after `if ...condition...`, so only advance to the next token if `then` is present
819
                thenToken = advance();
113✔
820
            }
821

822
            if (check(Lexeme.Newline) || check(Lexeme.Colon)) {
239✔
823
                //keep track of the current error count, because if the then branch fails,
824
                //we will trash them in favor of a single error on if
825
                let errorsLengthBeforeBlock = errors.length;
213✔
826

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

830
                let maybeThenBranch = block(Lexeme.EndIf, Lexeme.Else, Lexeme.ElseIf);
213✔
831
                if (!maybeThenBranch) {
213✔
832
                    //throw out any new errors created as a result of a `then` block parse failure.
833
                    //the block() function will discard the current line, so any discarded errors will
834
                    //resurface if they are legitimate, and not a result of a malformed if statement
835
                    errors.splice(errorsLengthBeforeBlock, errors.length - errorsLengthBeforeBlock);
2✔
836

837
                    //this whole if statement is bogus...add error to the if token and hard-fail
838
                    throw addError(
2✔
839
                        ifToken,
840
                        "Expected 'end if', 'else if', or 'else' to terminate 'then' block"
841
                    );
842
                }
843

844
                let blockEnd = maybeThenBranch.closingToken;
211✔
845
                if (blockEnd.kind === Lexeme.EndIf) {
211✔
846
                    endIfToken = blockEnd;
139✔
847
                }
848

849
                thenBranch = maybeThenBranch.body;
211✔
850

851
                // attempt to read a bunch of "else if" clauses
852
                while (blockEnd.kind === Lexeme.ElseIf) {
211✔
853
                    elseIfTokens.push(blockEnd);
13✔
854
                    let elseIfCondition = expression();
13✔
855
                    if (checkThen()) {
13✔
856
                        // `then` is optional after `else if ...condition...`, so only advance to the next token if `then` is present
857
                        advance();
8✔
858
                    }
859

860
                    let maybeElseIfThen = block(Lexeme.EndIf, Lexeme.Else, Lexeme.ElseIf);
13✔
861
                    if (!maybeElseIfThen) {
13!
862
                        throw addError(
×
863
                            peek(),
864
                            "Expected 'end if', 'else if', or 'else' to terminate 'then' block"
865
                        );
866
                    }
867

868
                    blockEnd = maybeElseIfThen.closingToken;
13✔
869
                    if (blockEnd.kind === Lexeme.EndIf) {
13✔
870
                        endIfToken = blockEnd;
1✔
871
                    }
872

873
                    elseIfBranches.push({
13✔
874
                        type: "ElseIf",
875
                        condition: elseIfCondition,
876
                        thenBranch: maybeElseIfThen.body,
877
                    });
878
                }
879

880
                if (blockEnd.kind === Lexeme.Else) {
211✔
881
                    elseToken = blockEnd;
71✔
882
                    let maybeElseBranch = block(Lexeme.EndIf);
71✔
883
                    if (!maybeElseBranch) {
71!
884
                        throw addError(peek(), "Expected 'end if' to terminate 'else' block");
×
885
                    }
886
                    elseBranch = maybeElseBranch.body;
71✔
887
                    endIfToken = maybeElseBranch.closingToken;
71✔
888

889
                    //ensure that single-line `if` statements have a colon right before 'end if'
890
                    if (ifToken.location.start.line === endIfToken.location.start.line) {
71✔
891
                        let index = tokens.indexOf(endIfToken);
2✔
892
                        let previousToken = tokens[index - 1];
2✔
893
                        if (previousToken.kind !== Lexeme.Colon) {
2✔
894
                            addError(endIfToken, "Expected ':' to preceed 'end if'");
1✔
895
                        }
896
                    }
897
                    match(Lexeme.Newline);
71✔
898
                } else {
899
                    if (!endIfToken) {
140!
900
                        throw addError(
×
901
                            blockEnd,
902
                            `Expected 'end if' to close 'if' statement started on line ${startingLine.start.line}`
903
                        );
904
                    }
905

906
                    //ensure that single-line `if` statements have a colon right before 'end if'
907
                    if (ifToken.location.start.line === endIfToken.location.start.line) {
140✔
908
                        let index = tokens.indexOf(endIfToken);
4✔
909
                        let previousToken = tokens[index - 1];
4✔
910
                        if (previousToken.kind !== Lexeme.Colon) {
4✔
911
                            addError(endIfToken, "Expected ':' to preceed 'end if'");
1✔
912
                        }
913
                    }
914
                    match(Lexeme.Newline);
140✔
915
                }
916
            } else {
917
                let maybeThenBranch = block(Lexeme.Newline, Lexeme.Eof, Lexeme.ElseIf, Lexeme.Else);
26✔
918
                if (!maybeThenBranch) {
26!
919
                    throw addError(
×
920
                        peek(),
921
                        "Expected a statement to follow 'if ...condition... then'"
922
                    );
923
                }
924
                thenBranch = maybeThenBranch.body;
26✔
925

926
                let closingToken = maybeThenBranch.closingToken;
26✔
927
                while (closingToken.kind === Lexeme.ElseIf) {
26✔
928
                    let elseIf = maybeThenBranch.closingToken;
3✔
929
                    elseIfTokens.push(elseIf);
3✔
930
                    let elseIfCondition = expression();
3✔
931
                    if (checkThen()) {
3✔
932
                        // `then` is optional after `else if ...condition...`, so only advance to the next token if `then` is present
933
                        advance();
2✔
934
                    }
935

936
                    let maybeElseIfBranch = block(
3✔
937
                        Lexeme.Newline,
938
                        Lexeme.Eof,
939
                        Lexeme.ElseIf,
940
                        Lexeme.Else
941
                    );
942
                    if (!maybeElseIfBranch) {
3!
943
                        throw addError(
×
944
                            peek(),
945
                            `Expected a statement to follow '${elseIf.text} ...condition... then'`
946
                        );
947
                    }
948
                    closingToken = maybeElseIfBranch.closingToken;
3✔
949

950
                    elseIfBranches.push({
3✔
951
                        type: "ElseIf",
952
                        condition: elseIfCondition,
953
                        thenBranch: maybeElseIfBranch.body,
954
                    });
955
                }
956

957
                if (
26✔
958
                    closingToken.kind !== Lexeme.Newline &&
33!
959
                    (closingToken.kind === Lexeme.Else || match(Lexeme.Else))
960
                ) {
961
                    elseToken = closingToken;
7✔
962
                    let maybeElseBranch = block(Lexeme.Newline, Lexeme.Eof);
7✔
963
                    if (!maybeElseBranch) {
7!
964
                        throw addError(peek(), `Expected a statement to follow 'else'`);
×
965
                    }
966
                    elseBranch = maybeElseBranch.body;
7✔
967
                }
968
            }
969

970
            return new Stmt.If(
237✔
971
                {
972
                    if: ifToken,
973
                    then: thenToken,
974
                    elseIfs: elseIfTokens,
975
                    endIf: endIfToken,
976
                    else: elseToken,
977
                },
978
                condition,
979
                thenBranch,
980
                elseIfBranches,
981
                elseBranch
982
            );
983
        }
984

985
        function setStatement(
986
            ...additionalTerminators: BlockTerminator[]
987
        ): Stmt.DottedSet | Stmt.IndexedSet | Stmt.Expression | Stmt.Increment {
988
            /**
989
             * Attempts to find an expression-statement or an increment statement.
990
             * While calls are valid expressions _and_ statements, increment (e.g. `foo++`)
991
             * statements aren't valid expressions. They _do_ however fall under the same parsing
992
             * priority as standalone function calls though, so we cann parse them in the same way.
993
             */
994
            function _expressionStatement(): Stmt.Expression | Stmt.Increment {
995
                let expressionStart = peek();
4,583✔
996

997
                if (check(Lexeme.PlusPlus, Lexeme.MinusMinus)) {
4,583✔
998
                    let operator = advance();
18✔
999

1000
                    if (check(Lexeme.PlusPlus, Lexeme.MinusMinus)) {
18✔
1001
                        throw addError(
1✔
1002
                            peek(),
1003
                            "Consecutive increment/decrement operators are not allowed"
1004
                        );
1005
                    } else if (expr instanceof Expr.Call) {
17✔
1006
                        throw addError(
1✔
1007
                            expressionStart,
1008
                            "Increment/decrement operators are not allowed on the result of a function call"
1009
                        );
1010
                    }
1011

1012
                    return new Stmt.Increment(expr, operator);
16✔
1013
                }
1014

1015
                if (!check(...additionalTerminators)) {
4,565✔
1016
                    consume(
4,564✔
1017
                        "Expected newline or ':' after expression statement",
1018
                        Lexeme.Newline,
1019
                        Lexeme.Colon,
1020
                        Lexeme.Eof
1021
                    );
1022
                }
1023

1024
                if (expr instanceof Expr.Call) {
4,565✔
1025
                    return new Stmt.Expression(expr);
4,562✔
1026
                }
1027

1028
                throw addError(
3✔
1029
                    expressionStart,
1030
                    "Expected statement or function call, but received an expression"
1031
                );
1032
            }
1033

1034
            let expr = call();
7,324✔
1035
            if (check(...assignmentOperators) && !(expr instanceof Expr.Call)) {
7,320✔
1036
                let left = expr;
2,737✔
1037
                let operator = advance();
2,737✔
1038
                let right = expression();
2,737✔
1039

1040
                // Create a dotted or indexed "set" based on the left-hand side's type
1041
                if (left instanceof Expr.IndexedGet) {
2,737✔
1042
                    consume(
65✔
1043
                        "Expected newline or ':' after indexed 'set' statement",
1044
                        Lexeme.Newline,
1045
                        Lexeme.Else,
1046
                        Lexeme.ElseIf,
1047
                        Lexeme.Colon,
1048
                        Lexeme.Eof
1049
                    );
1050

1051
                    return new Stmt.IndexedSet(
65✔
1052
                        left.obj,
1053
                        left.index,
1054
                        operator.kind === Lexeme.Equal
65✔
1055
                            ? right
1056
                            : new Expr.Binary(left, operator, right),
1057
                        left.closingSquare
1058
                    );
1059
                } else if (left instanceof Expr.DottedGet) {
2,672!
1060
                    consume(
2,672✔
1061
                        "Expected newline or ':' after dotted 'set' statement",
1062
                        Lexeme.Newline,
1063
                        Lexeme.Else,
1064
                        Lexeme.ElseIf,
1065
                        Lexeme.Colon,
1066
                        Lexeme.Eof
1067
                    );
1068

1069
                    return new Stmt.DottedSet(
2,672✔
1070
                        left.obj,
1071
                        left.name,
1072
                        operator.kind === Lexeme.Equal
2,672✔
1073
                            ? right
1074
                            : new Expr.Binary(left, operator, right)
1075
                    );
1076
                } else {
1077
                    return _expressionStatement();
×
1078
                }
1079
            } else {
1080
                return _expressionStatement();
4,583✔
1081
            }
1082
        }
1083

1084
        function printStatement(...additionalterminators: BlockTerminator[]): Stmt.Print {
1085
            let printKeyword = advance();
9,313✔
1086

1087
            let values: (Expr.Expression | Stmt.PrintSeparator.Tab | Stmt.PrintSeparator.Space)[] =
1088
                [];
9,313✔
1089

1090
            //print statements can be empty, so look for empty print conditions
1091
            if (isAtEnd() || check(Lexeme.Newline, Lexeme.Colon)) {
9,313✔
1092
                let emptyStringLiteral = new Expr.Literal(new BrsString(""), printKeyword.location);
1✔
1093
                values.push(emptyStringLiteral);
1✔
1094
            } else {
1095
                values.push(expression());
9,312✔
1096
            }
1097

1098
            while (!check(Lexeme.Newline, Lexeme.Colon, ...additionalterminators) && !isAtEnd()) {
9,313✔
1099
                if (check(Lexeme.Semicolon)) {
4,606✔
1100
                    values.push(advance() as Stmt.PrintSeparator.Space);
6✔
1101
                }
1102

1103
                if (check(Lexeme.Comma)) {
4,606✔
1104
                    values.push(advance() as Stmt.PrintSeparator.Tab);
3✔
1105
                }
1106

1107
                if (!check(Lexeme.Newline, Lexeme.Colon) && !isAtEnd()) {
4,606✔
1108
                    values.push(expression());
4,605✔
1109
                }
1110
            }
1111

1112
            if (!check(...additionalterminators)) {
9,313✔
1113
                consume(
9,305✔
1114
                    "Expected newline or ':' after printed values",
1115
                    Lexeme.Newline,
1116
                    Lexeme.Colon,
1117
                    Lexeme.Eof
1118
                );
1119
            }
1120

1121
            return new Stmt.Print({ print: printKeyword }, values);
9,313✔
1122
        }
1123

1124
        /**
1125
         * Parses a return statement with an optional return value.
1126
         * @returns an AST representation of a return statement.
1127
         */
1128
        function returnStatement(): Stmt.Return {
1129
            let tokens = { return: previous() };
1,118✔
1130

1131
            if (check(Lexeme.Colon, Lexeme.Newline, Lexeme.Eof)) {
1,118✔
1132
                while (match(Lexeme.Colon, Lexeme.Newline, Lexeme.Eof));
62✔
1133
                return new Stmt.Return(tokens);
62✔
1134
            }
1135

1136
            let toReturn = expression();
1,056✔
1137
            while (match(Lexeme.Newline, Lexeme.Colon));
1,056✔
1138

1139
            return new Stmt.Return(tokens, toReturn);
1,056✔
1140
        }
1141

1142
        /**
1143
         * Parses a `label` statement
1144
         * @returns an AST representation of an `label` statement.
1145
         */
1146
        function labelStatement() {
1147
            let tokens = {
2✔
1148
                identifier: advance(),
1149
                colon: advance(),
1150
            };
1151

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

1154
            return new Stmt.Label(tokens);
2✔
1155
        }
1156

1157
        /**
1158
         * Parses a `dim` statement
1159
         * @returns an AST representation of an `goto` statement.
1160
         */
1161
        function dimStatement() {
1162
            let dimToken = advance();
3✔
1163

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

1166
            match(Lexeme.LeftSquare);
3✔
1167

1168
            let dimensions: Expression[] = [expression()];
3✔
1169
            while (!match(Lexeme.RightSquare)) {
2✔
1170
                consume("Expected ',' after expression in 'dim' statement", Lexeme.Comma);
1✔
1171
                dimensions.push(expression());
1✔
1172
            }
1173
            let rightSquare = previous();
2✔
1174

1175
            let tokens = {
2✔
1176
                dim: dimToken,
1177
                closingBrace: rightSquare,
1178
            };
1179

1180
            return new Stmt.Dim(tokens, name as Identifier, dimensions);
2✔
1181
        }
1182

1183
        /**
1184
         * Parses a `goto` statement
1185
         * @returns an AST representation of an `goto` statement.
1186
         */
1187
        function gotoStatement() {
1188
            let tokens = {
3✔
1189
                goto: advance(),
1190
                label: consume("Expected label identifier after goto keyword", Lexeme.Identifier),
1191
            };
1192

1193
            while (match(Lexeme.Newline, Lexeme.Colon));
3✔
1194

1195
            return new Stmt.Goto(tokens);
3✔
1196
        }
1197

1198
        /**
1199
         * Parses an `end` statement
1200
         * @returns an AST representation of an `end` statement.
1201
         */
1202
        function endStatement() {
1203
            let tokens = { end: advance() };
1✔
1204

1205
            while (match(Lexeme.Newline));
1✔
1206

1207
            return new Stmt.End(tokens);
1✔
1208
        }
1209
        /**
1210
         * Parses a `stop` statement
1211
         * @returns an AST representation of a `stop` statement
1212
         */
1213
        function stopStatement() {
1214
            let tokens = { stop: advance() };
2✔
1215

1216
            while (match(Lexeme.Newline, Lexeme.Colon));
2✔
1217

1218
            return new Stmt.Stop(tokens);
2✔
1219
        }
1220

1221
        /**
1222
         * Parses a block, looking for a specific terminating Lexeme to denote completion.
1223
         * @param terminators the token(s) that signifies the end of this block; all other terminators are
1224
         *                    ignored.
1225
         */
1226
        function block(
1227
            ...terminators: BlockTerminator[]
1228
        ): { body: Stmt.Block; closingToken: Token } | undefined {
1229
            let startingToken = peek();
4,132✔
1230

1231
            let statementSeparators = [Lexeme.Colon];
4,132✔
1232
            if (!terminators.includes(Lexeme.Newline)) {
4,132✔
1233
                statementSeparators.push(Lexeme.Newline);
4,096✔
1234
            }
1235

1236
            while (match(...statementSeparators));
4,132✔
1237

1238
            let closingToken: Token | undefined;
1239
            const statements: Statement[] = [];
4,132✔
1240
            while (!check(...terminators) && !isAtEnd()) {
4,132✔
1241
                //grab the location of the current token
1242
                let loopCurrent = current;
20,826✔
1243
                let dec = declaration(...terminators);
20,826✔
1244

1245
                if (dec) {
20,826✔
1246
                    statements.push(dec);
20,821✔
1247
                } else {
1248
                    //something went wrong. reset to the top of the loop
1249
                    current = loopCurrent;
5✔
1250

1251
                    //scrap the entire line
1252
                    consumeUntil(Lexeme.Colon, Lexeme.Newline, Lexeme.Eof);
5✔
1253
                    //trash the newline character so we start the next iteraion on the next line
1254
                    advance();
5✔
1255
                }
1256

1257
                if (checkPrevious(...terminators)) {
20,826✔
1258
                    closingToken = previous();
13✔
1259
                    while (match(...statementSeparators));
13✔
1260
                    break;
13✔
1261
                } else {
1262
                    while (match(...statementSeparators));
20,813✔
1263
                }
1264
            }
1265

1266
            if (isAtEnd() && !terminators.includes(Lexeme.Eof)) {
4,132✔
1267
                return undefined;
2✔
1268
                // TODO: Figure out how to handle unterminated blocks well
1269
            }
1270

1271
            // consume the last terminator
1272
            if (check(...terminators) && !closingToken) {
4,130✔
1273
                closingToken = advance();
4,117✔
1274
            }
1275

1276
            if (!closingToken) {
4,130!
1277
                return undefined;
×
1278
            }
1279

1280
            //the block's location starts at the end of the preceeding token, and stops at the beginning of the `end` token
1281
            const location: Location = {
4,130✔
1282
                file: startingToken.location.file,
1283
                start: startingToken.location.start,
1284
                end: closingToken.location.start,
1285
            };
1286

1287
            return {
4,130✔
1288
                body: new Stmt.Block(statements, location),
1289
                closingToken,
1290
            };
1291
        }
1292

1293
        function expression(): Expression {
1294
            return anonymousFunction();
37,329✔
1295
        }
1296

1297
        function anonymousFunction(): Expression {
1298
            if (check(Lexeme.Sub, Lexeme.Function)) {
37,329✔
1299
                return functionDeclaration(true);
228✔
1300
            }
1301

1302
            return boolean();
37,101✔
1303
        }
1304

1305
        function boolean(): Expression {
1306
            let expr = relational();
37,101✔
1307

1308
            while (match(Lexeme.And, Lexeme.Or)) {
37,096✔
1309
                let operator = previous();
21✔
1310
                let right = relational();
21✔
1311
                expr = new Expr.Binary(expr, operator, right);
21✔
1312
            }
1313

1314
            return expr;
37,096✔
1315
        }
1316

1317
        function relational(): Expression {
1318
            let expr = bitshift();
37,345✔
1319

1320
            while (
37,340✔
1321
                match(
1322
                    Lexeme.Equal,
1323
                    Lexeme.LessGreater,
1324
                    Lexeme.Greater,
1325
                    Lexeme.GreaterEqual,
1326
                    Lexeme.Less,
1327
                    Lexeme.LessEqual
1328
                )
1329
            ) {
1330
                let operator = previous();
488✔
1331
                let right = bitshift();
488✔
1332
                expr = new Expr.Binary(expr, operator, right);
488✔
1333
            }
1334

1335
            return expr;
37,340✔
1336
        }
1337

1338
        function bitshift(): Expression {
1339
            let expr = additive();
37,833✔
1340

1341
            while (match(Lexeme.LeftShift, Lexeme.RightShift)) {
37,828✔
1342
                let operator = previous();
6✔
1343
                let right = additive();
6✔
1344
                expr = new Expr.Binary(expr, operator, right);
6✔
1345
            }
1346

1347
            return expr;
37,828✔
1348
        }
1349

1350
        function additive(): Expression {
1351
            let expr = multiplicative();
37,839✔
1352

1353
            while (match(Lexeme.Plus, Lexeme.Minus)) {
37,834✔
1354
                let operator = previous();
613✔
1355
                let right = multiplicative();
613✔
1356
                expr = new Expr.Binary(expr, operator, right);
613✔
1357
            }
1358

1359
            return expr;
37,834✔
1360
        }
1361

1362
        function multiplicative(): Expression {
1363
            let expr = exponential();
38,452✔
1364

1365
            while (match(Lexeme.Slash, Lexeme.Backslash, Lexeme.Star, Lexeme.Mod)) {
38,447✔
1366
                let operator = previous();
26✔
1367
                let right = exponential();
26✔
1368
                expr = new Expr.Binary(expr, operator, right);
26✔
1369
            }
1370

1371
            return expr;
38,447✔
1372
        }
1373

1374
        function exponential(): Expression {
1375
            let expr = prefixUnary();
38,478✔
1376

1377
            while (match(Lexeme.Caret)) {
38,473✔
1378
                let operator = previous();
9✔
1379
                let right = prefixUnary();
9✔
1380
                expr = new Expr.Binary(expr, operator, right);
9✔
1381
            }
1382

1383
            return expr;
38,473✔
1384
        }
1385

1386
        function prefixUnary(): Expression {
1387
            if (match(Lexeme.Not, Lexeme.Minus, Lexeme.Plus)) {
38,487✔
1388
                let operator = previous();
223✔
1389
                let right = relational();
223✔
1390
                return new Expr.Unary(operator, right);
223✔
1391
            }
1392

1393
            return call();
38,264✔
1394
        }
1395

1396
        function call(): Expression {
1397
            let expr = primary();
45,588✔
1398

1399
            function indexedGet() {
1400
                while (match(Lexeme.Newline));
412✔
1401

1402
                let index = expression();
412✔
1403

1404
                while (match(Lexeme.Newline));
412✔
1405
                let closingSquare = consume(
412✔
1406
                    "Expected ']' after array or object index",
1407
                    Lexeme.RightSquare
1408
                );
1409

1410
                expr = new Expr.IndexedGet(expr, index, closingSquare);
412✔
1411
            }
1412

1413
            function dottedGet() {}
1414

1415
            while (true) {
45,581✔
1416
                if (match(Lexeme.LeftParen)) {
77,714✔
1417
                    expr = finishCall(expr);
12,766✔
1418
                } else if (match(Lexeme.LeftSquare)) {
64,948✔
1419
                    indexedGet();
409✔
1420
                } else if (match(Lexeme.Dot)) {
64,539✔
1421
                    if (match(Lexeme.LeftSquare)) {
18,960✔
1422
                        indexedGet();
3✔
1423
                    } else {
1424
                        while (match(Lexeme.Newline));
18,957✔
1425

1426
                        let name = consume(
18,957✔
1427
                            "Expected property name after '.'",
1428
                            Lexeme.Identifier,
1429
                            ...allowedProperties
1430
                        );
1431

1432
                        // force it into an identifier so the AST makes some sense
1433
                        name.kind = Lexeme.Identifier;
18,956✔
1434

1435
                        expr = new Expr.DottedGet(expr, name as Identifier);
18,956✔
1436
                    }
1437
                } else {
1438
                    break;
45,579✔
1439
                }
1440
            }
1441

1442
            return expr;
45,579✔
1443
        }
1444

1445
        function finishCall(callee: Expression): Expression {
1446
            let args = [];
12,766✔
1447
            while (match(Lexeme.Newline));
12,766✔
1448

1449
            if (!check(Lexeme.RightParen)) {
12,766✔
1450
                do {
8,761✔
1451
                    while (match(Lexeme.Newline));
12,663✔
1452

1453
                    if (args.length >= Expr.Call.MaximumArguments) {
12,663!
1454
                        throw addError(
×
1455
                            peek(),
1456
                            `Cannot have more than ${Expr.Call.MaximumArguments} arguments`
1457
                        );
1458
                    }
1459
                    args.push(expression());
12,663✔
1460
                } while (match(Lexeme.Comma));
1461
            }
1462

1463
            while (match(Lexeme.Newline));
12,765✔
1464
            const closingParen = consume(
12,765✔
1465
                "Expected ')' after function call arguments",
1466
                Lexeme.RightParen
1467
            );
1468

1469
            return new Expr.Call(callee, closingParen, args);
12,765✔
1470
        }
1471

1472
        function primary(): Expression {
1473
            switch (true) {
45,588!
1474
                case match(Lexeme.False):
1475
                    return new Expr.Literal(BrsBoolean.False, previous().location);
917✔
1476
                case match(Lexeme.True):
1477
                    return new Expr.Literal(BrsBoolean.True, previous().location);
1,094✔
1478
                case match(Lexeme.Invalid):
1479
                    return new Expr.Literal(BrsInvalid.Instance, previous().location);
134✔
1480
                case match(
1481
                    Lexeme.Integer,
1482
                    Lexeme.LongInteger,
1483
                    Lexeme.Float,
1484
                    Lexeme.Double,
1485
                    Lexeme.String
1486
                ):
1487
                    return new Expr.Literal(previous().literal!, previous().location);
20,237✔
1488
                case match(Lexeme.Identifier):
1489
                    return new Expr.Variable(previous() as Identifier);
21,384✔
1490
                case match(Lexeme.LeftParen):
1491
                    let left = previous();
175✔
1492
                    let expr = expression();
175✔
1493
                    let right = consume(
175✔
1494
                        "Unmatched '(' - expected ')' after expression",
1495
                        Lexeme.RightParen
1496
                    );
1497
                    return new Expr.Grouping({ left, right }, expr);
175✔
1498
                case match(Lexeme.LeftSquare):
1499
                    let elements: Expression[] = [];
522✔
1500
                    let openingSquare = previous();
522✔
1501

1502
                    while (match(Lexeme.Newline));
522✔
1503

1504
                    if (!match(Lexeme.RightSquare)) {
522✔
1505
                        elements.push(expression());
517✔
1506

1507
                        while (match(Lexeme.Comma, Lexeme.Newline)) {
517✔
1508
                            while (match(Lexeme.Newline));
810✔
1509

1510
                            if (check(Lexeme.RightSquare)) {
810✔
1511
                                break;
3✔
1512
                            }
1513

1514
                            elements.push(expression());
807✔
1515
                        }
1516

1517
                        consume(
517✔
1518
                            "Unmatched '[' - expected ']' after array literal",
1519
                            Lexeme.RightSquare
1520
                        );
1521
                    }
1522

1523
                    let closingSquare = previous();
522✔
1524

1525
                    //consume("Expected newline or ':' after array literal", Lexeme.Newline, Lexeme.Colon, Lexeme.Eof);
1526
                    return new Expr.ArrayLiteral(elements, openingSquare, closingSquare);
522✔
1527
                case match(Lexeme.LeftBrace):
1528
                    let openingBrace = previous();
1,118✔
1529
                    let members: Expr.AAMember[] = [];
1,118✔
1530

1531
                    function key() {
1532
                        let k;
1533
                        if (check(Lexeme.Identifier, ...allowedProperties)) {
1,530✔
1534
                            k = new BrsString(advance().text!);
1,457✔
1535
                        } else if (check(Lexeme.String)) {
73!
1536
                            k = advance().literal! as BrsString;
73✔
1537
                        } else {
1538
                            throw addError(
×
1539
                                peek(),
1540
                                `Expected identifier or string as associative array key, but received '${
1541
                                    peek().text || ""
×
1542
                                }'`
1543
                            );
1544
                        }
1545

1546
                        consume(
1,530✔
1547
                            "Expected ':' between associative array key and value",
1548
                            Lexeme.Colon
1549
                        );
1550
                        return k;
1,530✔
1551
                    }
1552

1553
                    while (match(Lexeme.Newline));
1,118✔
1554

1555
                    if (!match(Lexeme.RightBrace)) {
1,118✔
1556
                        members.push({
886✔
1557
                            name: key(),
1558
                            value: expression(),
1559
                        });
1560

1561
                        while (match(Lexeme.Comma, Lexeme.Newline, Lexeme.Colon)) {
886✔
1562
                            while (match(Lexeme.Newline, Lexeme.Colon));
1,126✔
1563

1564
                            if (check(Lexeme.RightBrace)) {
1,126✔
1565
                                break;
482✔
1566
                            }
1567

1568
                            members.push({
644✔
1569
                                name: key(),
1570
                                value: expression(),
1571
                            });
1572
                        }
1573

1574
                        consume(
886✔
1575
                            "Unmatched '{' - expected '}' after associative array literal",
1576
                            Lexeme.RightBrace
1577
                        );
1578
                    }
1579

1580
                    let closingBrace = previous();
1,118✔
1581

1582
                    return new Expr.AALiteral(members, openingBrace, closingBrace);
1,118✔
1583
                case match(Lexeme.Pos, Lexeme.Tab):
1584
                    let token = Object.assign(previous(), {
×
1585
                        kind: Lexeme.Identifier,
1586
                    }) as Identifier;
1587
                    return new Expr.Variable(token);
×
1588
                case check(Lexeme.Function, Lexeme.Sub):
1589
                    return anonymousFunction();
×
1590
                default:
1591
                    throw addError(peek(), `Found unexpected token '${peek().text}'`);
7✔
1592
            }
1593
        }
1594

1595
        function match(...lexemes: Lexeme[]) {
1596
            for (let lexeme of lexemes) {
876,687✔
1597
                if (check(lexeme)) {
1,616,181✔
1598
                    advance();
96,705✔
1599
                    return true;
96,705✔
1600
                }
1601
            }
1602

1603
            return false;
779,982✔
1604
        }
1605

1606
        /**
1607
         * Consume tokens until one of the `stopLexemes` is encountered
1608
         * @param lexemes
1609
         * @return - the list of tokens consumed, EXCLUDING the `stopLexeme` (you can use `peek()` to see which one it was)
1610
         */
1611
        function consumeUntil(...stopLexemes: Lexeme[]) {
1612
            let result = [] as Token[];
12✔
1613
            //take tokens until we encounter one of the stopLexemes
1614
            while (!stopLexemes.includes(peek().kind)) {
12✔
1615
                result.push(advance());
10✔
1616
            }
1617
            return result;
12✔
1618
        }
1619

1620
        /**
1621
         * Checks that the next token is one of a list of lexemes and returns that token, and *advances past it*.
1622
         * If the next token is none of the provided lexemes, throws an error.
1623
         * @param message - the message to include in the thrown error if the next token isn't one of the provided `lexemes`
1624
         * @param lexemes - the set of `lexemes` to check for
1625
         *
1626
         * @see checkOrError
1627
         */
1628
        function consume(message: string, ...lexemes: Lexeme[]): Token {
1629
            let foundLexeme = lexemes
65,179✔
1630
                .map((lexeme) => peek().kind === lexeme)
952,621✔
1631
                .reduce((foundAny, foundCurrent) => foundAny || foundCurrent, false);
952,621✔
1632

1633
            if (foundLexeme) {
65,179✔
1634
                return advance();
65,178✔
1635
            }
1636
            throw addError(peek(), message);
1✔
1637
        }
1638

1639
        function advance(): Token {
1640
            if (!isAtEnd()) {
195,745✔
1641
                current++;
195,631✔
1642
            }
1643
            return previous();
195,745✔
1644
        }
1645

1646
        /**
1647
         * Checks that the next token is one of a list of lexemes and returns that token, but *does not advance past it*.
1648
         * If the next token is none of the provided lexemes, throws an error.
1649
         * @param message - the message to include in the thrown error if the next token isn't one of the provided `lexemes`
1650
         * @param lexemes - the set of `lexemes` to check for
1651
         *
1652
         * @see consume
1653
         */
1654
        function checkOrThrow(message: string, ...lexemes: Lexeme[]): Token {
1655
            let foundLexeme = lexemes
3,727✔
1656
                .map((lexeme) => peek().kind === lexeme)
7,447✔
1657
                .reduce((foundAny, foundCurrent) => foundAny || foundCurrent, false);
7,447✔
1658
            if (foundLexeme) {
3,727✔
1659
                return peek();
3,727✔
1660
            }
1661

1662
            throw addError(peek(), message);
×
1663
        }
1664

1665
        /**
1666
         * Check that the previous token matches one of the specified Lexemes
1667
         * @param lexemes
1668
         */
1669
        function checkPrevious(...lexemes: Lexeme[]) {
1670
            if (current === 0) {
20,826!
1671
                return false;
×
1672
            } else {
1673
                current--;
20,826✔
1674
                var result = check(...lexemes);
20,826✔
1675
                current++;
20,826✔
1676
                return result;
20,826✔
1677
            }
1678
        }
1679

1680
        function check(...lexemes: Lexeme[]) {
1681
            if (isAtEnd()) {
1,992,886✔
1682
                return false;
6,819✔
1683
            }
1684

1685
            return lexemes.some((lexeme) => peek().kind === lexeme);
2,189,010✔
1686
        }
1687

1688
        function checkNext(...lexemes: Lexeme[]) {
1689
            if (isAtEnd()) {
17,783!
1690
                return false;
×
1691
            }
1692

1693
            return lexemes.some((lexeme) => peekNext().kind === lexeme);
69,577✔
1694
        }
1695

1696
        function isAtEnd() {
1697
            return peek().kind === Lexeme.Eof;
2,349,631✔
1698
        }
1699

1700
        function peekNext() {
1701
            if (isAtEnd()) {
69,577!
1702
                return peek();
×
1703
            }
1704
            return tokens[current + 1];
69,577✔
1705
        }
1706

1707
        function peek() {
1708
            return tokens[current];
5,545,273✔
1709
        }
1710

1711
        function previous() {
1712
            return tokens[current - 1];
265,754✔
1713
        }
1714

1715
        function synchronize() {
1716
            advance(); // skip the erroneous token
15✔
1717

1718
            while (!isAtEnd()) {
15✔
1719
                if (previous().kind === Lexeme.Newline || previous().kind === Lexeme.Colon) {
17✔
1720
                    // newlines and ':' characters separate statements
1721
                    return;
2✔
1722
                }
1723

1724
                switch (peek().kind) {
15!
1725
                    case Lexeme.Function:
1726
                    case Lexeme.Sub:
1727
                    case Lexeme.If:
1728
                    case Lexeme.For:
1729
                    case Lexeme.ForEach:
1730
                    case Lexeme.While:
1731
                    case Lexeme.Print:
1732
                    case Lexeme.Return:
1733
                        // start parsing again from the next block starter or obvious
1734
                        // expression start
1735
                        return;
×
1736
                }
1737

1738
                advance();
15✔
1739
            }
1740
        }
1741
    }
1742
}
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