// SPDX-FileCopyrightText: 2022 Johannes Loher // // SPDX-License-Identifier: MIT import { describe, expect, it } from "vitest"; import { Lexer } from "../../src/expression-evaluation/lexer"; import type { Token } from "../../src/expression-evaluation/grammar"; describe("Lexer", () => { const singleOperatorTestCases: { input: string; expected: Token[] }[] = [ { input: "+", expected: [ { type: "+", pos: 0 }, { type: "eof", pos: 1 }, ], }, { input: "-", expected: [ { type: "-", pos: 0 }, { type: "eof", pos: 1 }, ], }, { input: "*", expected: [ { type: "*", pos: 0 }, { type: "eof", pos: 1 }, ], }, { input: "**", expected: [ { type: "**", pos: 0 }, { type: "eof", pos: 2 }, ], }, { input: "/", expected: [ { type: "/", pos: 0 }, { type: "eof", pos: 1 }, ], }, { input: "%", expected: [ { type: "%", pos: 0 }, { type: "eof", pos: 1 }, ], }, { input: "===", expected: [ { type: "===", pos: 0 }, { type: "eof", pos: 3 }, ], }, { input: "!==", expected: [ { type: "!==", pos: 0 }, { type: "eof", pos: 3 }, ], }, { input: "==", expected: [ { type: "==", pos: 0 }, { type: "eof", pos: 2 }, ], }, { input: "<", expected: [ { type: "<", pos: 0 }, { type: "eof", pos: 1 }, ], }, { input: "<=", expected: [ { type: "<=", pos: 0 }, { type: "eof", pos: 2 }, ], }, { input: ">", expected: [ { type: ">", pos: 0 }, { type: "eof", pos: 1 }, ], }, { input: ">=", expected: [ { type: ">=", pos: 0 }, { type: "eof", pos: 2 }, ], }, { input: "&&", expected: [ { type: "&&", pos: 0 }, { type: "eof", pos: 2 }, ], }, { input: "||", expected: [ { type: "||", pos: 0 }, { type: "eof", pos: 2 }, ], }, { input: "&", expected: [ { type: "&", pos: 0 }, { type: "eof", pos: 1 }, ], }, { input: "|", expected: [ { type: "|", pos: 0 }, { type: "eof", pos: 1 }, ], }, { input: "<<", expected: [ { type: "<<", pos: 0 }, { type: "eof", pos: 2 }, ], }, { input: ">>>", expected: [ { type: ">>>", pos: 0 }, { type: "eof", pos: 3 }, ], }, { input: ".", expected: [ { type: ".", pos: 0 }, { type: "eof", pos: 1 }, ], }, { input: "?.", expected: [ { type: "?.", pos: 0 }, { type: "eof", pos: 2 }, ], }, { input: "??", expected: [ { type: "??", pos: 0 }, { type: "eof", pos: 2 }, ], }, { input: "?", expected: [ { type: "?", pos: 0 }, { type: "eof", pos: 1 }, ], }, { input: ":", expected: [ { type: ":", pos: 0 }, { type: "eof", pos: 1 }, ], }, { input: "(", expected: [ { type: "(", pos: 0 }, { type: "eof", pos: 1 }, ], }, { input: ")", expected: [ { type: ")", pos: 0 }, { type: "eof", pos: 1 }, ], }, { input: "[", expected: [ { type: "[", pos: 0 }, { type: "eof", pos: 1 }, ], }, { input: "]", expected: [ { type: "]", pos: 0 }, { type: "eof", pos: 1 }, ], }, { input: ",", expected: [ { type: ",", pos: 0 }, { type: "eof", pos: 1 }, ], }, { input: "{", expected: [ { type: "{", pos: 0 }, { type: "eof", pos: 1 }, ], }, { input: "}", expected: [ { type: "}", pos: 0 }, { type: "eof", pos: 1 }, ], }, ]; const singleNumberTestCases: { input: string; expected: Token[] }[] = [ { input: "1", expected: [ { type: "number", symbol: "1", pos: 0 }, { type: "eof", pos: 1 }, ], }, { input: "42", expected: [ { type: "number", symbol: "42", pos: 0 }, { type: "eof", pos: 2 }, ], }, { input: "42.9", expected: [ { type: "number", symbol: "42.9", pos: 0 }, { type: "eof", pos: 4 }, ], }, { input: ".9", expected: [ { type: "number", symbol: ".9", pos: 0 }, { type: "eof", pos: 2 }, ], }, { input: "1_1", expected: [ { type: "number", symbol: "1_1", pos: 0 }, { type: "eof", pos: 3 }, ], }, { input: "10_1", expected: [ { type: "number", symbol: "10_1", pos: 0 }, { type: "eof", pos: 4 }, ], }, { input: "1_1_1", expected: [ { type: "number", symbol: "1_1_1", pos: 0 }, { type: "eof", pos: 5 }, ], }, { input: ".1_1", expected: [ { type: "number", symbol: ".1_1", pos: 0 }, { type: "eof", pos: 4 }, ], }, ]; const invalidNumberTestCases: { input: string; expected: Token[] }[] = [ { input: "1.1.1", expected: [{ type: "invalid", pos: 0 }] }, { input: "1__1", expected: [{ type: "invalid", pos: 0 }] }, { input: "1_", expected: [{ type: "invalid", pos: 0 }] }, { input: "1._1", expected: [{ type: "invalid", pos: 0 }] }, { input: "0_1", expected: [{ type: "invalid", pos: 0 }] }, { input: "00_1", expected: [{ type: "invalid", pos: 0 }] }, ]; const singleIdentifierTestCases: { input: string; expected: Token[] }[] = [ { input: "foo", expected: [ { type: "iden", symbol: "foo", pos: 0 }, { type: "eof", pos: 3 }, ], }, { input: "_foo", expected: [ { type: "iden", symbol: "_foo", pos: 0 }, { type: "eof", pos: 4 }, ], }, { input: "$foo", expected: [ { type: "iden", symbol: "$foo", pos: 0 }, { type: "eof", pos: 4 }, ], }, { input: "foo1", expected: [ { type: "iden", symbol: "foo1", pos: 0 }, { type: "eof", pos: 4 }, ], }, { input: "_foo1_", expected: [ { type: "iden", symbol: "_foo1_", pos: 0 }, { type: "eof", pos: 6 }, ], }, { input: "μ", expected: [ { type: "iden", symbol: "μ", pos: 0 }, { type: "eof", pos: 1 }, ], }, { input: "._1", expected: [ { type: ".", pos: 0 }, { type: "iden", symbol: "_1", pos: 1 }, { type: "eof", pos: 3 }, ], }, { input: "true", expected: [ { type: "iden", symbol: "true", pos: 0 }, { type: "eof", pos: 4 }, ], }, { input: "false", expected: [ { type: "iden", symbol: "false", pos: 0 }, { type: "eof", pos: 5 }, ], }, { input: "null", expected: [ { type: "iden", symbol: "null", pos: 0 }, { type: "eof", pos: 4 }, ], }, { input: "undefined", expected: [ { type: "iden", symbol: "undefined", pos: 0 }, { type: "eof", pos: 9 }, ], }, ]; const invalidIdentifierTestCases: { input: string; expected: Token[] }[] = [ { input: "1foo", expected: [ { type: "number", symbol: "1", pos: 0 }, { type: "iden", symbol: "foo", pos: 1 }, { type: "eof", pos: 4 }, ], }, { input: "↓", expected: [{ type: "invalid", pos: 0 }] }, { input: '"', expected: [{ type: "invalid", pos: 0 }] }, ]; const singleStringTestCases: { input: string; expected: Token[] }[] = [ { input: '""', expected: [ { type: "string", symbol: '""', pos: 0 }, { type: "eof", pos: 2 }, ], }, { input: '"foo"', expected: [ { type: "string", symbol: '"foo"', pos: 0 }, { type: "eof", pos: 5 }, ], }, { input: '"\\""', expected: [ { type: "string", symbol: '"\\""', pos: 0 }, { type: "eof", pos: 4 }, ], }, { input: '"\\\'"', expected: [ { type: "string", symbol: '"\\\'"', pos: 0 }, { type: "eof", pos: 4 }, ], }, { input: "''", expected: [ { type: "string", symbol: "''", pos: 0 }, { type: "eof", pos: 2 }, ], }, { input: "'foo'", expected: [ { type: "string", symbol: "'foo'", pos: 0 }, { type: "eof", pos: 5 }, ], }, { input: "'\\''", expected: [ { type: "string", symbol: "'\\''", pos: 0 }, { type: "eof", pos: 4 }, ], }, { input: "'\\\"'", expected: [ { type: "string", symbol: "'\\\"'", pos: 0 }, { type: "eof", pos: 4 }, ], }, { input: "``", expected: [ { type: "string", symbol: "``", pos: 0 }, { type: "eof", pos: 2 }, ], }, { input: "`foo`", expected: [ { type: "string", symbol: "`foo`", pos: 0 }, { type: "eof", pos: 5 }, ], }, { input: "`\\``", expected: [ { type: "string", symbol: "`\\``", pos: 0 }, { type: "eof", pos: 4 }, ], }, { input: '`\\"`', expected: [ { type: "string", symbol: '`\\"`', pos: 0 }, { type: "eof", pos: 4 }, ], }, ]; const invalidStringTestCases: { input: string; expected: Token[] }[] = [ { input: '"', expected: [{ type: "invalid", pos: 0 }] }, { input: '"\\"', expected: [{ type: "invalid", pos: 0 }] }, { input: "'", expected: [{ type: "invalid", pos: 0 }] }, { input: "'\\'", expected: [{ type: "invalid", pos: 0 }] }, ]; const whiteSpaceTestCases: { input: string; expected: Token[] }[] = [ { input: " ", expected: [{ type: "eof", pos: 1 }] }, { input: " ", expected: [{ type: "eof", pos: 3 }] }, { input: "\n", expected: [{ type: "eof", pos: 1 }] }, { input: " \n", expected: [{ type: "eof", pos: 2 }] }, { input: " ", expected: [{ type: "eof", pos: 1 }] }, ]; const complicatedTermTestCases: { input: string; expected: Token[] }[] = [ { input: "5x", expected: [ { type: "number", symbol: "5", pos: 0 }, { type: "iden", symbol: "x", pos: 1 }, { type: "eof", pos: 2 }, ], }, { input: "5*x", expected: [ { type: "number", symbol: "5", pos: 0 }, { type: "*", pos: 1 }, { type: "iden", symbol: "x", pos: 2 }, { type: "eof", pos: 3 }, ], }, { input: "5 * x", expected: [ { type: "number", symbol: "5", pos: 0 }, { type: "*", pos: 2 }, { type: "iden", symbol: "x", pos: 4 }, { type: "eof", pos: 5 }, ], }, { input: "(5 * 5 + 2) / 1.2 === 'foo'", expected: [ { type: "(", pos: 0 }, { type: "number", symbol: "5", pos: 1 }, { type: "*", pos: 3 }, { type: "number", symbol: "5", pos: 5 }, { type: "+", pos: 7 }, { type: "number", symbol: "2", pos: 9 }, { type: ")", pos: 10 }, { type: "/", pos: 12 }, { type: "number", symbol: "1.2", pos: 14 }, { type: "===", pos: 18 }, { type: "string", symbol: "'foo'", pos: 22 }, { type: "eof", pos: 27 }, ], }, { input: "(() => {console.log('foo'); return 1;})()", expected: [ { type: "(", pos: 0 }, { type: "(", pos: 1 }, { type: ")", pos: 2 }, { type: "invalid", pos: 4 }, ], }, { input: "(function() {console.log('foo'); return 1;})()", expected: [ { type: "(", pos: 0 }, { type: "iden", symbol: "function", pos: 1 }, { type: "(", pos: 9 }, { type: ")", pos: 10 }, { type: "{", pos: 12 }, { type: "iden", symbol: "console", pos: 13 }, { type: ".", pos: 20 }, { type: "iden", symbol: "log", pos: 21 }, { type: "(", pos: 24 }, { type: "string", symbol: "'foo'", pos: 25 }, { type: ")", pos: 30 }, { type: "invalid", pos: 31 }, ], }, { input: "'ranged' === 'ranged'", expected: [ { type: "string", symbol: "'ranged'", pos: 0 }, { type: "===", pos: 9 }, { type: "string", symbol: "'ranged'", pos: 13 }, { type: "eof", pos: 21 }, ], }, ]; it.each([ ...singleOperatorTestCases, ...singleNumberTestCases, ...invalidNumberTestCases, ...singleIdentifierTestCases, ...invalidIdentifierTestCases, ...singleStringTestCases, ...invalidStringTestCases, ...whiteSpaceTestCases, ...complicatedTermTestCases, ])("lexes $input correctly", ({ input, expected }) => { // when const result = consume(new Lexer(input)); // then expect(result).toEqual(expected); }); }); function consume<T>(iterable: Iterable<T>): T[] { const result: T[] = []; for (const value of iterable) { result.push(value); } return result; }