Repository: rokucommunity/brighterscript (ts)

Issue:
Support multi-dimensional array comma access syntax
Apparently multi-dimensional arrays support using a comma to access sub levels, rather than `[idx][idx]`. 
The parser currently fails with this syntax.
![image](https://github.com/rokucommunity/brighterscript/assets/2544493/cb1e2cf3-e027-4fa2-9bab-6266d7e654ba)


Here's some examples:

```brighterscript
myMultiArray = [["hello"]]

'currently supported syntax
print myMultiArray[0][0]
'currently broken syntax
print myMultiArray[0, 0]
```

This seems to work for however many levels of nesting you have.
```brighterscript
myMultiArray = [[[[[[[[["hello"]]]]]]]]]
print myMultiArray[0][0][0][0][0][0][0][0][0]
print myMultiArray[0, 0, 0, 0, 0, 0, 0, 0, 0]
```



Pull request description:
Fix parsing issues with multi-index IndexedSet and IndexedGet
Fixes parsing and transpile issues with mutli-index `IndexedGetExpression` and `IndexedSetStatement`.

The solution is a little "icky", because we can't break backwards compatability, so I had to add `additionalIndexes` to the two AST node types. In v1, we should merge those into a single `.indexes` prop on the nodes instead. 

Fixes #1048 

![image](https://github.com/rokucommunity/brighterscript/assets/2544493/b3051453-1374-4599-8232-ffb07345e8a7)


Patch:
diff --git a/src/files/BrsFile.spec.ts b/src/files/BrsFile.spec.ts
index a590f90e..2eb730a0 100644
--- a/src/files/BrsFile.spec.ts
+++ b/src/files/BrsFile.spec.ts
@@ -2582,6 +2582,40 @@ describe('BrsFile', () => {
             `);
         });
 
+        it('handles multi-index multi-dimensional arrays', () => {
+            testTranspile(`
+                sub main()
+                    myMultiArray = [[[[[[[[["hello"]]]]]]]]]
+                    myMultiArray[0][0][0][0][0][0][0][0][0] = "goodbye"
+                    print myMultiArray[0, 0, 0, 0, 0, 0, 0, 0, 0]
+                end sub
+            `, `
+                sub main()
+                    myMultiArray = [
+                        [
+                            [
+                                [
+                                    [
+                                        [
+                                            [
+                                                [
+                                                    [
+                                                        "hello"
+                                                    ]
+                                                ]
+                                            ]
+                                        ]
+                                    ]
+                                ]
+                            ]
+                        ]
+                    ]
+                    myMultiArray[0][0][0][0][0][0][0][0][0] = "goodbye"
+                    print myMultiArray[0, 0, 0, 0, 0, 0, 0, 0, 0]
+                end sub
+            `);
+        });
+
         it('transpiles calls to fully-qualified namespaced functions', () => {
             testTranspile(`
                 namespace NameA
@@ -3852,6 +3886,5 @@ describe('BrsFile', () => {
             program.validate();
             expectZeroDiagnostics(program);
         });
-
     });
 });
diff --git a/src/parser/Expression.ts b/src/parser/Expression.ts
index 7c7ed660..9c482fdf 100644
--- a/src/parser/Expression.ts
+++ b/src/parser/Expression.ts
@@ -513,28 +513,48 @@ export class IndexedGetExpression extends Expression {
          */
         public openingSquare: Token,
         public closingSquare: Token,
-        public questionDotToken?: Token //  ? or ?.
+        public questionDotToken?: Token, //  ? or ?.
+        /**
+         * More indexes, separated by commas
+         */
+        public additionalIndexes?: Expression[]
     ) {
         super();
         this.range = util.createBoundingRange(this.obj, this.openingSquare, this.questionDotToken, this.openingSquare, this.index, this.closingSquare);
+        this.additionalIndexes ??= [];
     }
 
     public readonly range: Range;
 
     transpile(state: BrsTranspileState) {
-        return [
+        const result = [];
+        result.push(
             ...this.obj.transpile(state),
             this.questionDotToken ? state.transpileToken(this.questionDotToken) : '',
-            state.transpileToken(this.openingSquare),
-            ...(this.index?.transpile(state) ?? []),
+            state.transpileToken(this.openingSquare)
+        );
+        const indexes = [this.index, ...this.additionalIndexes ?? []];
+        for (let i = 0; i < indexes.length; i++) {
+            //add comma between indexes
+            if (i > 0) {
+                result.push(', ');
+            }
+            let index = indexes[i];
+            result.push(
+                ...(index?.transpile(state) ?? [])
+            );
+        }
+        result.push(
             this.closingSquare ? state.transpileToken(this.closingSquare) : ''
-        ];
+        );
+        return result;
     }
 
     walk(visitor: WalkVisitor, options: WalkOptions) {
         if (options.walkMode & InternalWalkMode.walkExpressions) {
             walk(this, 'obj', visitor, options);
             walk(this, 'index', visitor, options);
+            walkArray(this.additionalIndexes, visitor, options, this);
         }
     }
 }
diff --git a/src/parser/Parser.ts b/src/parser/Parser.ts
index cdd73da5..2ca57a65 100644
--- a/src/parser/Parser.ts
+++ b/src/parser/Parser.ts
@@ -2053,7 +2053,8 @@ export class Parser {
                         ? right
                         : new BinaryExpression(left, operator, right),
                     left.openingSquare,
-                    left.closingSquare
+                    left.closingSquare,
+                    left.additionalIndexes
                 );
             } else if (isDottedGetExpression(left)) {
                 return new DottedSetStatement(
@@ -2437,22 +2438,36 @@ export class Parser {
     private indexedGet(expr: Expression) {
         let openingSquare = this.previous();
         let questionDotToken = this.getMatchingTokenAtOffset(-2, TokenKind.QuestionDot);
-        let index: Expression;
-        let closingSquare: Token;
+        let indexes: Expression[] = [];
+
+
+        //consume leading newlines
         while (this.match(TokenKind.Newline)) { }
+
         try {
-            index = this.expression();
+            indexes.push(
+                this.expression()
+            );
+            //consume additional indexes separated by commas
+            while (this.check(TokenKind.Comma)) {
+                //discard the comma
+                this.advance();
+                indexes.push(
+                    this.expression()
+                );
+            }
         } catch (error) {
             this.rethrowNonDiagnosticError(error);
         }
-
+        //consume trailing newlines
         while (this.match(TokenKind.Newline)) { }
-        closingSquare = this.tryConsume(
+
+        const closingSquare = this.tryConsume(
             DiagnosticMessages.expectedRightSquareBraceAfterArrayOrObjectIndex(),
             TokenKind.RightSquareBracket
         );
 
-        return new IndexedGetExpression(expr, index, openingSquare, closingSquare, questionDotToken);
+        return new IndexedGetExpression(expr, indexes.shift(), openingSquare, closingSquare, questionDotToken, indexes);
     }
 
     private newExpression() {
@@ -3002,6 +3017,11 @@ export class Parser {
         return this.previous()?.kind === tokenKind;
     }
 
+    /**
+     * Check that the next token kind is the expected kind
+     * @param tokenKind the expected next kind
+     * @returns true if the next tokenKind is the expected value
+     */
     private check(tokenKind: TokenKind): boolean {
         const nextKind = this.peek().kind;
         if (nextKind === TokenKind.Eof) {
diff --git a/src/parser/Statement.ts b/src/parser/Statement.ts
index 6f687b0e..c9ffbe7f 100644
--- a/src/parser/Statement.ts
+++ b/src/parser/Statement.ts
@@ -1043,7 +1043,8 @@ export class IndexedSetStatement extends Statement {
         readonly index: Expression,
         readonly value: Expression,
         readonly openingSquare: Token,
-        readonly closingSquare: Token
+        readonly closingSquare: Token,
+        readonly additionalIndexes?: Expression[]
     ) {
         super();
         this.range = util.createBoundingRange(
@@ -1053,6 +1054,7 @@ export class IndexedSetStatement extends Statement {
             closingSquare,
             value
         );
+        this.additionalIndexes ??= [];
     }
 
     public readonly range: Range;
@@ -1062,20 +1064,30 @@ export class IndexedSetStatement extends Statement {
         if (CompoundAssignmentOperators.includes((this.value as BinaryExpression)?.operator?.kind)) {
             return this.value.transpile(state);
         } else {
-            return [
+            const result = [];
+            result.push(
                 //obj
                 ...this.obj.transpile(state),
                 //   [
-                state.transpileToken(this.openingSquare),
-                //    index
-                ...this.index.transpile(state),
-                //         ]
+                state.transpileToken(this.openingSquare)
+            );
+            const indexes = [this.index, ...this.additionalIndexes ?? []];
+            for (let i = 0; i < indexes.length; i++) {
+                //add comma between indexes
+                if (i > 0) {
+                    result.push(', ');
+                }
+                let index = indexes[i];
+                result.push(
+                    ...(index?.transpile(state) ?? [])
+                );
+            }
+            result.push(
                 state.transpileToken(this.closingSquare),
-                //           =
                 ' = ',
-                //             value
                 ...this.value.transpile(state)
-            ];
+            );
+            return result;
         }
     }
 
@@ -1083,6 +1095,7 @@ export class IndexedSetStatement extends Statement {
         if (options.walkMode & InternalWalkMode.walkExpressions) {
             walk(this, 'obj', visitor, options);
             walk(this, 'index', visitor, options);
+            walkArray(this.additionalIndexes, visitor, options, this);
             walk(this, 'value', visitor, options);
         }
     }


Test patch:
diff --git a/src/parser/tests/expression/Indexing.spec.ts b/src/parser/tests/expression/Indexing.spec.ts
index 0a606aa9..34cf5b3d 100644
--- a/src/parser/tests/expression/Indexing.spec.ts
+++ b/src/parser/tests/expression/Indexing.spec.ts
@@ -5,10 +5,12 @@ import { TokenKind } from '../../../lexer/TokenKind';
 import { EOF, identifier, token } from '../Parser.spec';
 import { Range } from 'vscode-languageserver';
 import { DiagnosticMessages } from '../../../DiagnosticMessages';
+import type { IndexedSetStatement } from '../../Statement';
 import { AssignmentStatement } from '../../Statement';
 import { expectDiagnostics, expectDiagnosticsIncludes } from '../../../testHelpers.spec';
-import { isAssignmentStatement, isDottedGetExpression, isIndexedGetExpression, isVariableExpression } from '../../../astUtils/reflection';
+import { isAssignmentStatement, isDottedGetExpression, isIndexedGetExpression, isIndexedSetStatement, isLiteralExpression, isVariableExpression } from '../../../astUtils/reflection';
 import type { DottedGetExpression, IndexedGetExpression, VariableExpression } from '../../Expression';
+import { WalkMode } from '../../../astUtils/visitors';
 
 
 describe('parser indexing', () => {
@@ -185,6 +187,32 @@ describe('parser indexing', () => {
         });
     });
 
+    it('walks every index in the indexed get', () => {
+        const parser = Parser.parse(`
+            result = arr[0, 1, 2]
+        `);
+        const nodes = [];
+        parser.ast.findChild<AssignmentStatement>(isAssignmentStatement).value.walk((x) => {
+            if (isLiteralExpression(x)) {
+                nodes.push(x.token.text);
+            }
+        }, { walkMode: WalkMode.visitAllRecursive });
+        expect(nodes).to.eql(['0', '1', '2']);
+    });
+
+    it('walks every index in the indexed get', () => {
+        const parser = Parser.parse(`
+            arr[0, 1, 2] = "value"
+        `);
+        const nodes = [];
+        parser.ast.findChild<IndexedSetStatement>(isIndexedSetStatement).walk((x) => {
+            if (isLiteralExpression(x)) {
+                nodes.push(x.token.text);
+            }
+        }, { walkMode: WalkMode.visitAllRecursive });
+        expect(nodes).to.eql(['0', '1', '2', '"value"']);
+    });
+
     describe('multi-level', () => {
         it('dotted', () => {
             let { statements, diagnostics } = Parser.parse([