diff --git a/gleam.toml b/gleam.toml index e922181..5df695f 100644 --- a/gleam.toml +++ b/gleam.toml @@ -10,6 +10,7 @@ links = [] gleam_stdlib = ">= 0.44.0 and < 2.0.0" gleam_community_ansi = ">= 1.4.3 and < 2.0.0" houdini = ">= 1.1.0 and < 2.0.0" +splitter = ">= 1.0.0 and < 2.0.0" [dev-dependencies] gleeunit = ">= 1.0.0 and < 2.0.0" diff --git a/manifest.toml b/manifest.toml index ed314b5..c395cbf 100644 --- a/manifest.toml +++ b/manifest.toml @@ -19,6 +19,7 @@ packages = [ { name = "justin", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "justin", source = "hex", outer_checksum = "7FA0C6DB78640C6DC5FBFD59BF3456009F3F8B485BF6825E97E1EB44E9A1E2CD" }, { name = "rank", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "rank", source = "hex", outer_checksum = "5660E361F0E49CBB714CC57CC4C89C63415D8986F05B2DA0C719D5642FAD91C9" }, { name = "simplifile", version = "2.2.1", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "C88E0EE2D509F6D86EB55161D631657675AA7684DAB83822F7E59EB93D9A60E3" }, + { name = "splitter", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "splitter", source = "hex", outer_checksum = "128FC521EE33B0012E3E64D5B55168586BC1B9C8D7B0D0CA223B68B0D770A547" }, { name = "term_size", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "term_size", source = "hex", outer_checksum = "D00BD2BC8FB3EBB7E6AE076F3F1FF2AC9D5ED1805F004D0896C784D06C6645F1" }, { name = "trie_again", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "trie_again", source = "hex", outer_checksum = "5B19176F52B1BD98831B57FDC97BD1F88C8A403D6D8C63471407E78598E27184" }, ] @@ -30,3 +31,4 @@ gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" } gleeunit = { version = ">= 1.0.0 and < 2.0.0" } houdini = { version = ">= 1.1.0 and < 2.0.0" } simplifile = { version = ">= 2.2.1 and < 3.0.0" } +splitter = { version = ">= 1.0.0 and < 2.0.0" } diff --git a/src/just.gleam b/src/just.gleam index 6c90bf3..868b22d 100644 --- a/src/just.gleam +++ b/src/just.gleam @@ -1,6 +1,7 @@ import gleam/list import gleam/string import just/token.{type Token} +import splitter.{type Splitter} pub opaque type Lexer { Lexer( @@ -10,6 +11,18 @@ pub opaque type Lexer { strict_mode: Bool, mode: LexerMode, errors: List(Error), + splitters: Splitters, + ) +} + +type Splitters { + Splitters( + string: Splitter, + multiline_comment: Splitter, + template: Splitter, + until_end_of_line: Splitter, + regex_in_group: Splitter, + regex_regular: Splitter, ) } @@ -57,6 +70,20 @@ pub fn new(source: String) -> Lexer { strict_mode: False, mode: TreatSlashAsRegex, errors: [], + splitters: make_splitters(), + ) +} + +fn make_splitters() -> Splitters { + Splitters( + string: splitter.new(["\"", "'", "\\", "\n", "\r"]), + multiline_comment: splitter.new(["*/"]), + template: splitter.new(["`", "${", "\\"]), + until_end_of_line: splitter.new(["\n", "\r", "\u{2028}", "\u{2029}"]), + regex_regular: splitter.new([ + "/", "[", "]", "\n", "\r", "\u{2028}", "\u{2029}", "\\", + ]), + regex_in_group: splitter.new(["]", "\n", "\r", "\u{2028}", "\u{2029}", "\\"]), ) } @@ -87,7 +114,7 @@ fn maybe_lex_hashbang_comment(lexer: Lexer) -> #(Lexer, List(Token)) { let #(lexer, contents) = lexer |> advance(source) - |> lex_until_end_of_line("") + |> lex_until_end_of_line #(lexer, case lexer.ignore_comments { True -> [] @@ -163,14 +190,14 @@ fn next(lexer: Lexer) -> #(Lexer, Token) { ) "//" <> source -> { - let #(lexer, contents) = lex_until_end_of_line(advance(lexer, source), "") + let #(lexer, contents) = lex_until_end_of_line(advance(lexer, source)) maybe_token( lexer, token.SingleLineComment(contents), !lexer.ignore_comments, ) } - "/*" <> source -> lex_multiline_comment(advance(lexer, source), "") + "/*" <> source -> lex_multiline_comment(advance(lexer, source)) "0b" as prefix <> source | "0B" as prefix <> source -> lex_radix_number(advance(lexer, source), 2, prefix, False) @@ -383,25 +410,20 @@ fn next(lexer: Lexer) -> #(Lexer, Token) { } } -fn lex_multiline_comment(lexer: Lexer, lexed: String) -> #(Lexer, Token) { - case lexer.source { - "*/" <> source -> +fn lex_multiline_comment(lexer: Lexer) -> #(Lexer, Token) { + case splitter.split(lexer.splitters.multiline_comment, lexer.source) { + #(before, "", "") -> maybe_token( - advance(lexer, source), - token.MultiLineComment(lexed), + error(advance(lexer, ""), UnterminatedComment), + token.UnterminatedComment(before), + !lexer.ignore_comments, + ) + #(before, _, after) -> + maybe_token( + advance(lexer, after), + token.MultiLineComment(before), !lexer.ignore_comments, ) - _ -> - case string.pop_grapheme(lexer.source) { - Error(_) -> - maybe_token( - error(lexer, UnterminatedComment), - token.UnterminatedComment(lexed), - !lexer.ignore_comments, - ) - Ok(#(char, source)) -> - lex_multiline_comment(advance(lexer, source), lexed <> char) - } } } @@ -704,60 +726,68 @@ fn ensure_no_letters_after_numbers(lexer: Lexer) -> Lexer { } fn lex_string(lexer: Lexer, quote: String, contents: String) -> #(Lexer, Token) { - case string.pop_grapheme(lexer.source) { - Error(_) -> #( - error(lexer, UnterminatedString), - token.UnterminatedString(quote:, contents:), - ) - Ok(#("\n", _source)) | Ok(#("\r", _source)) -> #( - error(lexer, UnterminatedString), - token.UnterminatedString(quote:, contents:), + let #(before, split, after) = + splitter.split(lexer.splitters.string, lexer.source) + case split { + "" -> #( + error(advance(lexer, after), UnterminatedString), + token.UnterminatedString(quote:, contents: contents <> before), ) - Ok(#(character, source)) if character == quote -> #( - advance(lexer, source), - token.String(quote:, contents:), + "\r" | "\n" -> #( + error(advance(lexer, split <> after), UnterminatedString), + token.UnterminatedString(quote:, contents: contents <> before), ) - Ok(#("\\", source)) -> - case string.pop_grapheme(source) { + + "\\" -> + case string.pop_grapheme(after) { Error(_) -> #( - error(lexer, UnterminatedString), + error(advance(lexer, after), UnterminatedString), token.UnterminatedString(quote:, contents:), ) Ok(#(character, source)) -> lex_string( advance(lexer, source), quote, - contents <> "\\" <> character, + contents <> before <> "\\" <> character, ) } - Ok(#(character, source)) -> - lex_string(advance(lexer, source), quote, contents <> character) + + _ if split == quote -> #( + advance(lexer, after), + token.String(quote:, contents: contents <> before), + ) + + // Here, we've split on a quote which doesn't match the current string. + // In this case, we must continue lexing until we find a quote of the + // correct kind. + _ -> lex_string(advance(lexer, after), quote, contents <> before <> split) } } fn lex_template_head(lexer: Lexer, lexed: String) -> #(Lexer, Token) { - case lexer.source { - "${" <> source -> #(advance(lexer, source), token.TemplateHead(lexed)) - "`" <> source -> #(advance(lexer, source), token.String("`", lexed)) - "\\" <> source -> - case string.pop_grapheme(source) { - Error(_) -> #( - error(lexer, UnterminatedString), - token.UnterminatedString("`", lexed), - ) - Ok(#(character, source)) -> - lex_template_head(advance(lexer, source), lexed <> "\\" <> character) - } - _ -> - case string.pop_grapheme(lexer.source) { + let #(before, split, after) = + splitter.split(lexer.splitters.template, lexer.source) + + case split { + "${" -> #(advance(lexer, after), token.TemplateHead(lexed <> before)) + "\\" -> + case string.pop_grapheme(after) { Error(_) -> #( - error(lexer, UnterminatedString), - token.UnterminatedString("`", lexed), + error(advance(lexer, after), UnterminatedString), + token.UnterminatedString("`", lexed <> before), ) Ok(#(character, source)) -> - lex_template_head(advance(lexer, source), lexed <> character) + lex_template_head( + advance(lexer, source), + lexed <> before <> "\\" <> character, + ) } + "" -> #( + error(advance(lexer, after), UnterminatedString), + token.UnterminatedString("`", lexed <> before), + ) + _ -> #(advance(lexer, after), token.String("`", lexed <> before)) } } @@ -772,48 +802,43 @@ fn lex_template_parts( mode: LexTemplateMode, ) -> #(Lexer, List(Token)) { case mode { - LexTemplate(lexed) -> - case lexer.source { - "`" <> source -> #(advance(lexer, source), [ - token.TemplateTail(lexed), - ..tokens - ]) - "${" <> source -> { - let lexer = advance(lexer, source) - let token = token.TemplateMiddle(lexed) + LexTemplate(lexed) -> { + let #(before, split, after) = + splitter.split(lexer.splitters.template, lexer.source) + + case split { + "${" -> { + let lexer = advance(lexer, after) + let token = token.TemplateMiddle(lexed <> before) lex_template_parts( update_mode_with_token(lexer, token), [token, ..tokens], LexTokens(0), ) } - "\\" <> source -> - case string.pop_grapheme(source) { - Error(_) -> #(error(lexer, UnterminatedTemplate), [ - token.UnterminatedTemplate(lexed), - ..tokens - ]) - Ok(#(character, source)) -> - lex_template_parts( - advance(lexer, source), - tokens, - LexTemplate(lexed <> "\\" <> character), - ) - } - _ -> - case string.pop_grapheme(lexer.source) { + "\\" -> + case string.pop_grapheme(after) { Error(_) -> #(error(lexer, UnterminatedTemplate), [ - token.UnterminatedTemplate(lexed), + token.UnterminatedTemplate(lexed <> before), ..tokens ]) Ok(#(character, source)) -> lex_template_parts( advance(lexer, source), tokens, - LexTemplate(lexed <> character), + LexTemplate(lexed <> before <> "\\" <> character), ) } + "" -> #(error(advance(lexer, after), UnterminatedTemplate), [ + token.UnterminatedTemplate(lexed <> before), + ..tokens + ]) + _ -> #(advance(lexer, after), [ + token.TemplateTail(lexed <> before), + ..tokens + ]) } + } LexTokens(bracket_level) -> case next(lexer) { @@ -843,43 +868,38 @@ fn lex_template_parts( } fn lex_regex(lexer: Lexer, lexed: String, in_group: Bool) -> #(Lexer, Token) { - case lexer.source { - "/" <> source if !in_group -> { - let lexer = advance(lexer, source) + let splitter = case in_group { + False -> lexer.splitters.regex_regular + True -> lexer.splitters.regex_in_group + } + let #(before, split, after) = splitter.split(splitter, lexer.source) + case split { + "/" -> { + let lexer = advance(lexer, after) let #(lexer, flags) = lex_identifier(lexer, "") - #(lexer, token.RegularExpression(contents: lexed, flags:)) + #(lexer, token.RegularExpression(contents: lexed <> before, flags:)) } - "[" <> source -> lex_regex(advance(lexer, source), lexed <> "[", True) - "]" <> source -> lex_regex(advance(lexer, source), lexed <> "]", False) - "\n" <> _source - | "\r" <> _source - | "\u{2028}" <> _source - | "\u{2029}" <> _source -> #( - error(lexer, UnterminatedRegularExpression), - token.UnterminatedRegularExpression(lexed), - ) - "\\" <> source -> - case string.pop_grapheme(source) { + "[" -> lex_regex(advance(lexer, after), lexed <> before <> "[", True) + "]" -> lex_regex(advance(lexer, after), lexed <> before <> "]", False) + + "\\" -> + case string.pop_grapheme(after) { Error(_) -> #( - error(lexer, UnterminatedRegularExpression), - token.UnterminatedRegularExpression(lexed), + error(advance(lexer, after), UnterminatedRegularExpression), + token.UnterminatedRegularExpression(lexed <> before), ) Ok(#(character, source)) -> lex_regex( advance(lexer, source), - lexed <> "\\" <> character, + lexed <> before <> "\\" <> character, in_group, ) } - _ -> - case string.pop_grapheme(lexer.source) { - Error(_) -> #( - error(lexer, UnterminatedRegularExpression), - token.UnterminatedRegularExpression(lexed), - ) - Ok(#(character, source)) -> - lex_regex(advance(lexer, source), lexed <> character, in_group) - } + + _ -> #( + error(advance(lexer, split <> after), UnterminatedRegularExpression), + token.UnterminatedRegularExpression(lexed <> before), + ) } } @@ -987,19 +1007,10 @@ fn whitespace(lexer: Lexer, lexed: String) -> #(Lexer, Token) { } } -fn lex_until_end_of_line(lexer: Lexer, lexed: String) -> #(Lexer, String) { - case lexer.source { - "\n" <> _source - | "\r" <> _source - | "\u{2028}" <> _source - | "\u{2029}" <> _source -> #(lexer, lexed) - _ -> - case string.pop_grapheme(lexer.source) { - Error(_) -> #(lexer, lexed) - Ok(#(char, source)) -> - lex_until_end_of_line(advance(lexer, source), lexed <> char) - } - } +fn lex_until_end_of_line(lexer: Lexer) -> #(Lexer, String) { + let #(before, split, after) = + splitter.split(lexer.splitters.until_end_of_line, lexer.source) + #(advance(lexer, split <> after), before) } fn advance(lexer: Lexer, source: String) -> Lexer { diff --git a/test/just_test.gleam b/test/just_test.gleam index 5275eb1..d060f29 100644 --- a/test/just_test.gleam +++ b/test/just_test.gleam @@ -9,9 +9,16 @@ pub fn main() -> Nil { gleeunit.main() } -fn assert_roundtrip(src: String) -> Nil { +fn assert_roundtrip(src: String, allow_errors: Bool) -> Nil { let #(tokens, errors) = just.new(src) |> just.tokenise - let assert [] = errors + case allow_errors { + True -> Nil + False -> { + let assert [] = errors + Nil + } + } + just.to_source(tokens) |> should.equal(src) } @@ -69,15 +76,29 @@ pub fn stdlib_ffi_roundtrip_test() { "build/packages/gleam_stdlib/src/gleam_stdlib_decode_ffi.mjs", ) - assert_roundtrip(stdlib_ffi_file) - assert_roundtrip(stdlib_dict_file) - assert_roundtrip(stdlib_decode_file) + assert_roundtrip(stdlib_ffi_file, False) + assert_roundtrip(stdlib_dict_file, False) + assert_roundtrip(stdlib_decode_file, False) } pub fn tokens_roundtrip_test() { let assert Ok(example_file) = simplifile.read("test/tokens.js") - assert_roundtrip(example_file) + assert_roundtrip(example_file, False) +} + +pub fn errors_roundtrip_test() { + assert_roundtrip( + " +let unknown = @; +let ustring = 'This string does not finish +let uregex = /uh oh +let badNumber = 0xabcdefg; +/* +This comment spans the rest of the program. +", + True, + ) } pub fn regex_token_test() {