diff --git a/src/parser/scanner.ts b/src/parser/scanner.ts index c1b07983..3b3aa6cd 100644 --- a/src/parser/scanner.ts +++ b/src/parser/scanner.ts @@ -38,7 +38,7 @@ export class Scanner implements ITokenStream { /** * カーソル位置にあるトークンの種類を取得します。 */ - public get kind(): TokenKind { + public getKind(): TokenKind { return this.token.kind; } @@ -74,8 +74,8 @@ export class Scanner implements ITokenStream { * 一致しなかった場合には文法エラーを発生させます。 */ public expect(kind: TokenKind): void { - if (this.kind !== kind) { - throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[this.kind]}`, this.token.loc); + if (this.getKind() !== kind) { + throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[this.getKind()]}`, this.token.loc); } } diff --git a/src/parser/streams/token-stream.ts b/src/parser/streams/token-stream.ts index c0fadcaa..0ef2fe23 100644 --- a/src/parser/streams/token-stream.ts +++ b/src/parser/streams/token-stream.ts @@ -14,7 +14,7 @@ export interface ITokenStream { /** * カーソル位置にあるトークンの種類を取得します。 */ - get kind(): TokenKind; + getKind(): TokenKind; /** * カーソル位置を次のトークンへ進めます。 @@ -70,7 +70,7 @@ export class TokenStream implements ITokenStream { /** * カーソル位置にあるトークンの種類を取得します。 */ - public get kind(): TokenKind { + public getKind(): TokenKind { return this.token.kind; } @@ -100,8 +100,8 @@ export class TokenStream implements ITokenStream { * 一致しなかった場合には文法エラーを発生させます。 */ public expect(kind: TokenKind): void { - if (this.kind !== kind) { - throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[this.kind]}`, this.token.loc); + if (this.getKind() !== kind) { + throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[this.getKind()]}`, this.token.loc); } } diff --git a/src/parser/syntaxes/common.ts b/src/parser/syntaxes/common.ts index b4bd9dc0..9f32c970 100644 --- a/src/parser/syntaxes/common.ts +++ b/src/parser/syntaxes/common.ts @@ -16,17 +16,17 @@ export function parseParams(s: ITokenStream): { name: string, argType?: Ast.Node s.nextWith(TokenKind.OpenParen); - if (s.kind === TokenKind.NewLine) { + if (s.getKind() === TokenKind.NewLine) { s.next(); } - while (s.kind !== TokenKind.CloseParen) { + while (s.getKind() !== TokenKind.CloseParen) { s.expect(TokenKind.Identifier); const name = s.token.value!; s.next(); let type; - if ((s.kind as TokenKind) === TokenKind.Colon) { + if (s.getKind() === TokenKind.Colon) { s.next(); type = parseType(s); } @@ -34,14 +34,14 @@ export function parseParams(s: ITokenStream): { name: string, argType?: Ast.Node items.push({ name, argType: type }); // separator - switch (s.kind as TokenKind) { + switch (s.getKind()) { case TokenKind.NewLine: { s.next(); break; } case TokenKind.Comma: { s.next(); - if (s.kind === TokenKind.NewLine) { + if (s.getKind() === TokenKind.NewLine) { s.next(); } break; @@ -68,19 +68,19 @@ export function parseParams(s: ITokenStream): { name: string, argType?: Ast.Node export function parseBlock(s: ITokenStream): Ast.Node[] { s.nextWith(TokenKind.OpenBrace); - while (s.kind === TokenKind.NewLine) { + while (s.getKind() === TokenKind.NewLine) { s.next(); } const steps: Ast.Node[] = []; - while (s.kind !== TokenKind.CloseBrace) { + while (s.getKind() !== TokenKind.CloseBrace) { steps.push(parseStatement(s)); // terminator - switch (s.kind as TokenKind) { + switch (s.getKind()) { case TokenKind.NewLine: case TokenKind.SemiColon: { - while ([TokenKind.NewLine, TokenKind.SemiColon].includes(s.kind)) { + while ([TokenKind.NewLine, TokenKind.SemiColon].includes(s.getKind())) { s.next(); } break; @@ -102,7 +102,7 @@ export function parseBlock(s: ITokenStream): Ast.Node[] { //#region Type export function parseType(s: ITokenStream): Ast.Node { - if (s.kind === TokenKind.At) { + if (s.getKind() === TokenKind.At) { return parseFnType(s); } else { return parseNamedType(s); @@ -122,9 +122,9 @@ function parseFnType(s: ITokenStream): Ast.Node { s.nextWith(TokenKind.OpenParen); const params: Ast.Node[] = []; - while (s.kind !== TokenKind.CloseParen) { + while (s.getKind() !== TokenKind.CloseParen) { if (params.length > 0) { - switch (s.kind as TokenKind) { + switch (s.getKind()) { case TokenKind.Comma: { s.next(); break; @@ -160,7 +160,7 @@ function parseNamedType(s: ITokenStream): Ast.Node { // inner type let inner = null; - if (s.kind === TokenKind.Lt) { + if (s.getKind() === TokenKind.Lt) { s.next(); inner = parseType(s); s.nextWith(TokenKind.Gt); diff --git a/src/parser/syntaxes/expressions.ts b/src/parser/syntaxes/expressions.ts index 3adda1bd..439b8fc1 100644 --- a/src/parser/syntaxes/expressions.ts +++ b/src/parser/syntaxes/expressions.ts @@ -53,11 +53,11 @@ const operators: OpInfo[] = [ function parsePrefix(s: ITokenStream, minBp: number): Ast.Node { const loc = s.token.loc; - const op = s.kind; + const op = s.getKind(); s.next(); // 改行のエスケープ - if (s.kind === TokenKind.BackSlash) { + if (s.getKind() === TokenKind.BackSlash) { s.next(); s.nextWith(TokenKind.NewLine); } @@ -96,11 +96,11 @@ function parsePrefix(s: ITokenStream, minBp: number): Ast.Node { function parseInfix(s: ITokenStream, left: Ast.Node, minBp: number): Ast.Node { const loc = s.token.loc; - const op = s.kind; + const op = s.getKind(); s.next(); // 改行のエスケープ - if (s.kind === TokenKind.BackSlash) { + if (s.getKind() === TokenKind.BackSlash) { s.next(); s.nextWith(TokenKind.NewLine); } @@ -169,7 +169,7 @@ function parseInfix(s: ITokenStream, left: Ast.Node, minBp: number): Ast.Node { function parsePostfix(s: ITokenStream, expr: Ast.Node): Ast.Node { const loc = s.token.loc; - const op = s.kind; + const op = s.getKind(); switch (op) { case TokenKind.OpenParen: { @@ -194,7 +194,7 @@ function parsePostfix(s: ITokenStream, expr: Ast.Node): Ast.Node { function parseAtom(s: ITokenStream, isStatic: boolean): Ast.Node { const loc = s.token.loc; - switch (s.kind) { + switch (s.getKind()) { case TokenKind.IfKeyword: { if (isStatic) break; return parseIf(s); @@ -230,7 +230,7 @@ function parseAtom(s: ITokenStream, isStatic: boolean): Ast.Node { // スキャナで埋め込み式として事前に読み取っておいたトークン列をパースする const exprStream = new TokenStream(element.children!); const expr = parseExpr(exprStream, false); - if (exprStream.kind !== TokenKind.EOF) { + if (exprStream.getKind() !== TokenKind.EOF) { throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[exprStream.token.kind]}`, exprStream.token.loc); } values.push(expr); @@ -258,7 +258,7 @@ function parseAtom(s: ITokenStream, isStatic: boolean): Ast.Node { } case TokenKind.TrueKeyword: case TokenKind.FalseKeyword: { - const value = (s.kind === TokenKind.TrueKeyword); + const value = (s.getKind() === TokenKind.TrueKeyword); s.next(); return NODE('bool', { value }, loc); } @@ -283,7 +283,7 @@ function parseAtom(s: ITokenStream, isStatic: boolean): Ast.Node { return expr; } } - throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[s.kind]}`, loc); + throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[s.getKind()]}`, loc); } /** @@ -295,22 +295,22 @@ function parseCall(s: ITokenStream, target: Ast.Node): Ast.Node { s.nextWith(TokenKind.OpenParen); - if (s.kind === TokenKind.NewLine) { + if (s.getKind() === TokenKind.NewLine) { s.next(); } - while (s.kind !== TokenKind.CloseParen) { + while (s.getKind() !== TokenKind.CloseParen) { items.push(parseExpr(s, false)); // separator - switch (s.kind as TokenKind) { + switch (s.getKind()) { case TokenKind.NewLine: { s.next(); break; } case TokenKind.Comma: { s.next(); - if (s.kind === TokenKind.NewLine) { + if (s.getKind() === TokenKind.NewLine) { s.next(); } break; @@ -344,23 +344,23 @@ function parseIf(s: ITokenStream): Ast.Node { const cond = parseExpr(s, false); const then = parseBlockOrStatement(s); - if (s.kind === TokenKind.NewLine && [TokenKind.ElifKeyword, TokenKind.ElseKeyword].includes(s.lookahead(1).kind)) { + if (s.getKind() === TokenKind.NewLine && [TokenKind.ElifKeyword, TokenKind.ElseKeyword].includes(s.lookahead(1).kind)) { s.next(); } const elseif: { cond: Ast.Node, then: Ast.Node }[] = []; - while (s.kind === TokenKind.ElifKeyword) { + while (s.getKind() === TokenKind.ElifKeyword) { s.next(); const elifCond = parseExpr(s, false); const elifThen = parseBlockOrStatement(s); - if ((s.kind as TokenKind) === TokenKind.NewLine && [TokenKind.ElifKeyword, TokenKind.ElseKeyword].includes(s.lookahead(1).kind)) { + if ((s.getKind()) === TokenKind.NewLine && [TokenKind.ElifKeyword, TokenKind.ElseKeyword].includes(s.lookahead(1).kind)) { s.next(); } elseif.push({ cond: elifCond, then: elifThen }); } let _else = undefined; - if (s.kind === TokenKind.ElseKeyword) { + if (s.getKind() === TokenKind.ElseKeyword) { s.next(); _else = parseBlockOrStatement(s); } @@ -381,7 +381,7 @@ function parseFnExpr(s: ITokenStream): Ast.Node { const params = parseParams(s); let type; - if ((s.kind as TokenKind) === TokenKind.Colon) { + if ((s.getKind()) === TokenKind.Colon) { s.next(); type = parseType(s); } @@ -405,12 +405,12 @@ function parseMatch(s: ITokenStream): Ast.Node { s.nextWith(TokenKind.OpenBrace); - if (s.kind === TokenKind.NewLine) { + if (s.getKind() === TokenKind.NewLine) { s.next(); } const qs: { q: Ast.Node, a: Ast.Node }[] = []; - while (s.kind !== TokenKind.DefaultKeyword && s.kind !== TokenKind.CloseBrace) { + while (s.getKind() !== TokenKind.DefaultKeyword && s.getKind() !== TokenKind.CloseBrace) { s.nextWith(TokenKind.CaseKeyword); const q = parseExpr(s, false); s.nextWith(TokenKind.Arrow); @@ -418,14 +418,14 @@ function parseMatch(s: ITokenStream): Ast.Node { qs.push({ q, a }); // separator - switch (s.kind as TokenKind) { + switch (s.getKind()) { case TokenKind.NewLine: { s.next(); break; } case TokenKind.Comma: { s.next(); - if (s.kind === TokenKind.NewLine) { + if (s.getKind() === TokenKind.NewLine) { s.next(); } break; @@ -441,20 +441,20 @@ function parseMatch(s: ITokenStream): Ast.Node { } let x; - if (s.kind === TokenKind.DefaultKeyword) { + if (s.getKind() === TokenKind.DefaultKeyword) { s.next(); s.nextWith(TokenKind.Arrow); x = parseBlockOrStatement(s); // separator - switch (s.kind as TokenKind) { + switch (s.getKind()) { case TokenKind.NewLine: { s.next(); break; } case TokenKind.Comma: { s.next(); - if ((s.kind as TokenKind) === TokenKind.NewLine) { + if ((s.getKind()) === TokenKind.NewLine) { s.next(); } break; @@ -510,7 +510,7 @@ function parseReference(s: ITokenStream): Ast.Node { const segs: string[] = []; while (true) { if (segs.length > 0) { - if (s.kind === TokenKind.Colon) { + if (s.getKind() === TokenKind.Colon) { if (s.token.hasLeftSpacing) { throw new AiScriptSyntaxError('Cannot use spaces in a reference.', s.token.loc); } @@ -539,12 +539,12 @@ function parseObject(s: ITokenStream, isStatic: boolean): Ast.Node { s.nextWith(TokenKind.OpenBrace); - if (s.kind === TokenKind.NewLine) { + while (s.getKind() === TokenKind.NewLine) { s.next(); } const map = new Map(); - while (s.kind !== TokenKind.CloseBrace) { + while (s.getKind() !== TokenKind.CloseBrace) { s.expect(TokenKind.Identifier); const k = s.token.value!; s.next(); @@ -556,14 +556,11 @@ function parseObject(s: ITokenStream, isStatic: boolean): Ast.Node { map.set(k, v); // separator - switch (s.kind as TokenKind) { - case TokenKind.NewLine: { - s.next(); - break; - } + switch (s.getKind()) { + case TokenKind.NewLine: case TokenKind.Comma: { s.next(); - if (s.kind === TokenKind.NewLine) { + while (s.getKind() === TokenKind.NewLine) { s.next(); } break; @@ -592,23 +589,20 @@ function parseArray(s: ITokenStream, isStatic: boolean): Ast.Node { s.nextWith(TokenKind.OpenBracket); - if (s.kind === TokenKind.NewLine) { + while (s.getKind() === TokenKind.NewLine) { s.next(); } const value = []; - while (s.kind !== TokenKind.CloseBracket) { + while (s.getKind() !== TokenKind.CloseBracket) { value.push(parseExpr(s, isStatic)); // separator - switch (s.kind as TokenKind) { - case TokenKind.NewLine: { - s.next(); - break; - } + switch (s.getKind()) { + case TokenKind.NewLine: case TokenKind.Comma: { s.next(); - if (s.kind === TokenKind.NewLine) { + while (s.getKind() === TokenKind.NewLine) { s.next(); } break; @@ -640,7 +634,7 @@ function parsePratt(s: ITokenStream, minBp: number): Ast.Node { let left: Ast.Node; - const tokenKind = s.kind; + const tokenKind = s.getKind(); const prefix = operators.find((x): x is PrefixInfo => x.opKind === 'prefix' && x.kind === tokenKind); if (prefix != null) { left = parsePrefix(s, prefix.bp); @@ -650,12 +644,12 @@ function parsePratt(s: ITokenStream, minBp: number): Ast.Node { while (true) { // 改行のエスケープ - if (s.kind === TokenKind.BackSlash) { + if (s.getKind() === TokenKind.BackSlash) { s.next(); s.nextWith(TokenKind.NewLine); } - const tokenKind = s.kind; + const tokenKind = s.getKind(); const postfix = operators.find((x): x is PostfixInfo => x.opKind === 'postfix' && x.kind === tokenKind); if (postfix != null) { diff --git a/src/parser/syntaxes/statements.ts b/src/parser/syntaxes/statements.ts index 4c816a5b..717ad579 100644 --- a/src/parser/syntaxes/statements.ts +++ b/src/parser/syntaxes/statements.ts @@ -16,7 +16,7 @@ import type { ITokenStream } from '../streams/token-stream.js'; export function parseStatement(s: ITokenStream): Ast.Node { const loc = s.token.loc; - switch (s.kind) { + switch (s.getKind()) { case TokenKind.VarKeyword: case TokenKind.LetKeyword: { return parseVarDef(s); @@ -63,7 +63,7 @@ export function parseStatement(s: ITokenStream): Ast.Node { } export function parseDefStatement(s: ITokenStream): Ast.Node { - switch (s.kind) { + switch (s.getKind()) { case TokenKind.VarKeyword: case TokenKind.LetKeyword: { return parseVarDef(s); @@ -72,7 +72,7 @@ export function parseDefStatement(s: ITokenStream): Ast.Node { return parseFnDef(s); } default: { - throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[s.kind]}`, s.token.loc); + throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[s.getKind()]}`, s.token.loc); } } } @@ -85,7 +85,7 @@ export function parseDefStatement(s: ITokenStream): Ast.Node { export function parseBlockOrStatement(s: ITokenStream): Ast.Node { const loc = s.token.loc; - if (s.kind === TokenKind.OpenBrace) { + if (s.getKind() === TokenKind.OpenBrace) { const statements = parseBlock(s); return NODE('block', { statements }, loc); } else { @@ -102,7 +102,7 @@ function parseVarDef(s: ITokenStream): Ast.Node { const loc = s.token.loc; let mut; - switch (s.kind) { + switch (s.getKind()) { case TokenKind.LetKeyword: { mut = false; break; @@ -112,7 +112,7 @@ function parseVarDef(s: ITokenStream): Ast.Node { break; } default: { - throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[s.kind]}`, s.token.loc); + throw new AiScriptSyntaxError(`unexpected token: ${TokenKind[s.getKind()]}`, s.token.loc); } } s.next(); @@ -122,14 +122,14 @@ function parseVarDef(s: ITokenStream): Ast.Node { s.next(); let type; - if ((s.kind as TokenKind) === TokenKind.Colon) { + if (s.getKind() === TokenKind.Colon) { s.next(); type = parseType(s); } s.nextWith(TokenKind.Eq); - if ((s.kind as TokenKind) === TokenKind.NewLine) { + if (s.getKind() === TokenKind.NewLine) { s.next(); } @@ -155,7 +155,7 @@ function parseFnDef(s: ITokenStream): Ast.Node { const params = parseParams(s); let type; - if ((s.kind as TokenKind) === TokenKind.Colon) { + if (s.getKind() === TokenKind.Colon) { s.next(); type = parseType(s); } @@ -199,7 +199,7 @@ function parseEach(s: ITokenStream): Ast.Node { s.nextWith(TokenKind.EachKeyword); - if (s.kind === TokenKind.OpenParen) { + if (s.getKind() === TokenKind.OpenParen) { hasParen = true; s.next(); } @@ -210,7 +210,7 @@ function parseEach(s: ITokenStream): Ast.Node { const name = s.token.value!; s.next(); - if (s.kind === TokenKind.Comma) { + if (s.getKind() === TokenKind.Comma) { s.next(); } else { throw new AiScriptSyntaxError('separator expected', s.token.loc); @@ -237,12 +237,12 @@ function parseFor(s: ITokenStream): Ast.Node { s.nextWith(TokenKind.ForKeyword); - if (s.kind === TokenKind.OpenParen) { + if (s.getKind() === TokenKind.OpenParen) { hasParen = true; s.next(); } - if (s.kind === TokenKind.LetKeyword) { + if (s.getKind() === TokenKind.LetKeyword) { // range syntax s.next(); @@ -253,14 +253,14 @@ function parseFor(s: ITokenStream): Ast.Node { s.next(); let _from; - if ((s.kind as TokenKind) === TokenKind.Eq) { + if (s.getKind() === TokenKind.Eq) { s.next(); _from = parseExpr(s, false); } else { _from = NODE('num', { value: 0 }, identLoc); } - if ((s.kind as TokenKind) === TokenKind.Comma) { + if (s.getKind() === TokenKind.Comma) { s.next(); } else { throw new AiScriptSyntaxError('separator expected', s.token.loc); @@ -318,7 +318,7 @@ function parseReturn(s: ITokenStream): Ast.Node { */ function parseStatementWithAttr(s: ITokenStream): Ast.Node { const attrs: Ast.Attribute[] = []; - while (s.kind === TokenKind.OpenSharpBracket) { + while (s.getKind() === TokenKind.OpenSharpBracket) { attrs.push(parseAttr(s) as Ast.Attribute); s.nextWith(TokenKind.NewLine); } @@ -352,7 +352,7 @@ function parseAttr(s: ITokenStream): Ast.Node { s.next(); let value; - if (s.kind !== TokenKind.CloseBracket) { + if (s.getKind() !== TokenKind.CloseBracket) { value = parseExpr(s, true); } else { value = NODE('bool', { value: true }, loc); @@ -385,7 +385,7 @@ function tryParseAssign(s: ITokenStream, dest: Ast.Node): Ast.Node | undefined { const loc = s.token.loc; // Assign - switch (s.kind) { + switch (s.getKind()) { case TokenKind.Eq: { s.next(); const expr = parseExpr(s, false); diff --git a/src/parser/syntaxes/toplevel.ts b/src/parser/syntaxes/toplevel.ts index c0969e65..ebd32d28 100644 --- a/src/parser/syntaxes/toplevel.ts +++ b/src/parser/syntaxes/toplevel.ts @@ -15,12 +15,12 @@ import type { ITokenStream } from '../streams/token-stream.js'; export function parseTopLevel(s: ITokenStream): Ast.Node[] { const nodes: Ast.Node[] = []; - while (s.kind === TokenKind.NewLine) { + while (s.getKind() === TokenKind.NewLine) { s.next(); } - while (s.kind !== TokenKind.EOF) { - switch (s.kind) { + while (s.getKind() !== TokenKind.EOF) { + switch (s.getKind()) { case TokenKind.Colon2: { nodes.push(parseNamespace(s)); break; @@ -36,10 +36,10 @@ export function parseTopLevel(s: ITokenStream): Ast.Node[] { } // terminator - switch (s.kind as TokenKind) { + switch (s.getKind()) { case TokenKind.NewLine: case TokenKind.SemiColon: { - while ([TokenKind.NewLine, TokenKind.SemiColon].includes(s.kind)) { + while ([TokenKind.NewLine, TokenKind.SemiColon].includes(s.getKind())) { s.next(); } break; @@ -73,12 +73,12 @@ export function parseNamespace(s: ITokenStream): Ast.Node { const members: Ast.Node[] = []; s.nextWith(TokenKind.OpenBrace); - while (s.kind === TokenKind.NewLine) { + while (s.getKind() === TokenKind.NewLine) { s.next(); } - while (s.kind !== TokenKind.CloseBrace) { - switch (s.kind) { + while (s.getKind() !== TokenKind.CloseBrace) { + switch (s.getKind()) { case TokenKind.VarKeyword: case TokenKind.LetKeyword: case TokenKind.At: { @@ -92,10 +92,10 @@ export function parseNamespace(s: ITokenStream): Ast.Node { } // terminator - switch (s.kind as TokenKind) { + switch (s.getKind()) { case TokenKind.NewLine: case TokenKind.SemiColon: { - while ([TokenKind.NewLine, TokenKind.SemiColon].includes(s.kind)) { + while ([TokenKind.NewLine, TokenKind.SemiColon].includes(s.getKind())) { s.next(); } break; @@ -124,7 +124,7 @@ export function parseMeta(s: ITokenStream): Ast.Node { s.nextWith(TokenKind.Sharp3); let name = null; - if (s.kind === TokenKind.Identifier) { + if (s.getKind() === TokenKind.Identifier) { name = s.token.value!; s.next(); } diff --git a/test/index.ts b/test/index.ts index f5bdde2e..57b49c3c 100644 --- a/test/index.ts +++ b/test/index.ts @@ -674,6 +674,20 @@ describe('separator', () => { eq(res, NUM(2)); }); + test.concurrent('multi line, multi newlines', async () => { + const res = await exe(` + let x = { + + a: 1 + + b: 2 + + } + <: x.b + `); + eq(res, NUM(2)); + }); + test.concurrent('multi line with comma', async () => { const res = await exe(` let x = { @@ -714,6 +728,20 @@ describe('separator', () => { eq(res, NUM(2)); }); + test.concurrent('multi line, multi newlines', async () => { + const res = await exe(` + let x = [ + + 1 + + 2 + + ] + <: x[1] + `); + eq(res, NUM(2)); + }); + test.concurrent('multi line with comma', async () => { const res = await exe(` let x = [ @@ -725,6 +753,45 @@ describe('separator', () => { eq(res, NUM(2)); }); + test.concurrent('multi line with comma, multi newlines', async () => { + const res = await exe(` + let x = [ + + 1, + + 2 + + ] + <: x[1] + `); + eq(res, NUM(2)); + }); + + test.concurrent('multi line with comma and tail comma', async () => { + const res = await exe(` + let x = [ + 1, + 2, + ] + <: x[1] + `); + eq(res, NUM(2)); + }); + + test.concurrent('multi line with comma and tail comma, multi newlines', async () => { + const res = await exe(` + let x = [ + + 1, + + 2, + + ] + <: x[1] + `); + eq(res, NUM(2)); + }); + test.concurrent('single line', async () => { const res = await exe(` let x=[1,2]