ghall_test.gleam

  1import birdie
  2import gleam/int
  3import gleam/list
  4import gleam/string
  5import gleeunit
  6import nibble.{Expected}
  7import nibble/lexer.{Span, Token}
  8import node
  9import parser
 10import quasi_lexer
 11import snapshot_helpers
 12
 13pub fn main() -> Nil {
 14  gleeunit.main()
 15}
 16
 17pub fn simple_quasi_lexer_test() {
 18  quasi_lexer.chars()
 19  |> quasi_lexer.run(on: "let x1 = e1")
 20  |> list.index_map(fn(token, index) {
 21    let Token(span, lexeme, value) = token
 22    assert lexeme == value
 23
 24    let Span(row_start, col_start, row_end, col_end) = span
 25    assert row_start == row_end && row_start == 1
 26    assert col_start == index + 1
 27    assert col_end == col_start + 1
 28  })
 29}
 30
 31pub fn quasi_lexer_off_by_one_test() {
 32  let input = "let x1 =\n  e1"
 33  let tokens = quasi_lexer.chars() |> quasi_lexer.run(on: input)
 34
 35  snapshot_helpers.snap_lexer_output(
 36    input,
 37    tokens,
 38    "Quasi lexer spans with multiline input",
 39  )
 40}
 41
 42pub fn parse_let_successfully_test() {
 43  let input = "let"
 44  let tokens = quasi_lexer.chars() |> quasi_lexer.run(on: input)
 45  let parser = parser.exact_string("let", node.Let)
 46  let assert Ok(_) = nibble.run(tokens, parser)
 47}
 48
 49pub fn parse_let_failing_test() {
 50  let input = "lt"
 51  let tokens = quasi_lexer.chars() |> quasi_lexer.run(on: input)
 52  let parser = parser.exact_string("let", node.Let)
 53  let assert Error(error) = nibble.run(tokens, parser)
 54  let assert [nibble.DeadEnd(Span(_, cs, _, _), Expected(msg, got: got), _)] =
 55    error
 56
 57  let snap =
 58    "Msg: "
 59    <> msg
 60    <> "\n"
 61    <> "Got: "
 62    <> got
 63    <> "\n"
 64    <> "At column: "
 65    <> int.to_string(cs)
 66    <> "\n"
 67
 68  birdie.snap(snap, title: "Should fail to parse 'lt' as node.Let")
 69}
 70
 71pub fn parse_unix_line_ending_test() {
 72  let input = "\n"
 73  let tokens = quasi_lexer.chars() |> quasi_lexer.run(on: input)
 74  let parser = parser.end_of_line()
 75  let assert Ok(node.EndOfLine) = nibble.run(tokens, parser)
 76}
 77
 78pub fn parse_windows_line_ending_test() {
 79  let input = "\r\n"
 80  let tokens = quasi_lexer.chars() |> quasi_lexer.run(on: input)
 81  let parser = parser.end_of_line()
 82  let assert Ok(node.EndOfLine) = nibble.run(tokens, parser)
 83}
 84
 85pub fn parse_line_ending_fails_on_lone_carriage_return_test() {
 86  let input = "\r"
 87  let tokens = quasi_lexer.chars() |> quasi_lexer.run(on: input)
 88  let parser = parser.end_of_line()
 89  let assert Error(error) = nibble.run(tokens, parser)
 90
 91  snapshot_helpers.snap_parse_error(
 92    input,
 93    error,
 94    "Line ending should reject lone carriage return",
 95  )
 96}
 97
 98pub fn parse_line_ending_fails_on_other_chars_test() {
 99  let input = "x"
100  let tokens = quasi_lexer.chars() |> quasi_lexer.run(on: input)
101  let parser = parser.end_of_line()
102  let assert Error(_) = nibble.run(tokens, parser)
103}
104
105pub fn parse_line_ending_after_content_test() {
106  let input = "let\n"
107  let tokens = quasi_lexer.chars() |> quasi_lexer.run(on: input)
108  let parser = {
109    use _ <- nibble.do(parser.let_keyword())
110    parser.end_of_line()
111  }
112  let assert Ok(node.EndOfLine) = nibble.run(tokens, parser)
113}
114
115pub fn parse_multiple_line_endings_test() {
116  let input = "\n\r\n\n"
117  let tokens = quasi_lexer.chars() |> quasi_lexer.run(on: input)
118  let parser = {
119    use eol1 <- nibble.do(parser.end_of_line())
120    use eol2 <- nibble.do(parser.end_of_line())
121    use eol3 <- nibble.do(parser.end_of_line())
122    nibble.return([eol1, eol2, eol3])
123  }
124  let assert Ok(nodes) = nibble.run(tokens, parser)
125
126  snapshot_helpers.snap_parse_success(
127    input,
128    nodes,
129    "Multiple line endings should all parse as EndOfLine",
130  )
131}
132
133pub fn demo_visual_error_rendering_test() {
134  let input = "let x = 42\r"
135  let tokens = quasi_lexer.chars() |> quasi_lexer.run(on: input)
136
137  // Try to parse "let" followed by a line ending
138  let parser = {
139    use _ <- nibble.do(parser.let_keyword())
140    use _ <- nibble.do(parser.exact_string(" x = 42", node.Let))
141    parser.end_of_line()
142  }
143
144  let assert Error(errors) = nibble.run(tokens, parser)
145
146  snapshot_helpers.snap_parse_error(
147    input,
148    errors,
149    "Visual error demo: shows escaped chars, spans, and parser context",
150  )
151}
152
153// Tests for printable parser (%x20-7F)
154
155pub fn parse_printable_space_test() {
156  let input = " "
157  let tokens = quasi_lexer.chars() |> quasi_lexer.run(on: input)
158  let parser = parser.printable()
159  let assert Ok(node.Printable(" ")) = nibble.run(tokens, parser)
160}
161
162pub fn parse_printable_ascii_test() {
163  let input = "a"
164  let tokens = quasi_lexer.chars() |> quasi_lexer.run(on: input)
165  let parser = parser.printable()
166  let assert Ok(node.Printable("a")) = nibble.run(tokens, parser)
167}
168
169pub fn parse_printable_tilde_test() {
170  let input = "~"
171  let tokens = quasi_lexer.chars() |> quasi_lexer.run(on: input)
172  let parser = parser.printable()
173  let assert Ok(node.Printable("~")) = nibble.run(tokens, parser)
174}
175
176pub fn parse_printable_rejects_tab_test() {
177  let input = "\t"
178  let tokens = quasi_lexer.chars() |> quasi_lexer.run(on: input)
179  let parser = parser.printable()
180  let assert Error(_) = nibble.run(tokens, parser)
181}
182
183pub fn parse_printable_rejects_newline_test() {
184  let input = "\n"
185  let tokens = quasi_lexer.chars() |> quasi_lexer.run(on: input)
186  let parser = parser.printable()
187  let assert Error(_) = nibble.run(tokens, parser)
188}
189
190pub fn parse_printable_rejects_non_ascii_test() {
191  let input = "é"
192  let tokens = quasi_lexer.chars() |> quasi_lexer.run(on: input)
193  let parser = parser.printable()
194  let assert Error(_) = nibble.run(tokens, parser)
195}
196
197// Tests for valid-non-ascii parser
198
199pub fn parse_valid_non_ascii_latin_test() {
200  let input = "é"
201  // é is U+00E9, in range 0x80-0xD7FF
202  let tokens = quasi_lexer.chars() |> quasi_lexer.run(on: input)
203  let parser = parser.valid_non_ascii()
204  let assert Ok(node.ValidNonAscii("é")) = nibble.run(tokens, parser)
205}
206
207pub fn parse_valid_non_ascii_emoji_test() {
208  let input = "🎉"
209  // 🎉 is U+1F389, in range 0x10000-0x1FFFD
210  let tokens = quasi_lexer.chars() |> quasi_lexer.run(on: input)
211  let parser = parser.valid_non_ascii()
212  let assert Ok(node.ValidNonAscii("🎉")) = nibble.run(tokens, parser)
213}
214
215pub fn parse_valid_non_ascii_chinese_test() {
216  let input = ""
217  // 中 is U+4E2D, in range 0xE000-0xFFFD
218  let tokens = quasi_lexer.chars() |> quasi_lexer.run(on: input)
219  let parser = parser.valid_non_ascii()
220  let assert Ok(node.ValidNonAscii("")) = nibble.run(tokens, parser)
221}
222
223pub fn parse_valid_non_ascii_rejects_ascii_test() {
224  let input = "a"
225  let tokens = quasi_lexer.chars() |> quasi_lexer.run(on: input)
226  let parser = parser.valid_non_ascii()
227  let assert Error(_) = nibble.run(tokens, parser)
228}
229
230pub fn parse_valid_non_ascii_rejects_tab_test() {
231  let input = "\t"
232  let tokens = quasi_lexer.chars() |> quasi_lexer.run(on: input)
233  let parser = parser.valid_non_ascii()
234  let assert Error(_) = nibble.run(tokens, parser)
235}
236
237// Tests for not-end-of-line parser
238
239pub fn parse_not_end_of_line_printable_test() {
240  let input = "a"
241  let tokens = quasi_lexer.chars() |> quasi_lexer.run(on: input)
242  let parser = parser.not_end_of_line()
243  let assert Ok(node.NotEndOfLine(node.Printable("a"))) =
244    nibble.run(tokens, parser)
245}
246
247pub fn parse_not_end_of_line_space_test() {
248  let input = " "
249  let tokens = quasi_lexer.chars() |> quasi_lexer.run(on: input)
250  let parser = parser.not_end_of_line()
251  let assert Ok(node.NotEndOfLine(node.Printable(" "))) =
252    nibble.run(tokens, parser)
253}
254
255pub fn parse_not_end_of_line_tab_test() {
256  let input = "\t"
257  let tokens = quasi_lexer.chars() |> quasi_lexer.run(on: input)
258  let parser = parser.not_end_of_line()
259  let assert Ok(node.NotEndOfLine(node.Tab)) = nibble.run(tokens, parser)
260}
261
262pub fn parse_not_end_of_line_valid_non_ascii_test() {
263  let input = "λ"
264  let tokens = quasi_lexer.chars() |> quasi_lexer.run(on: input)
265  let parser = parser.not_end_of_line()
266  let assert Ok(node.NotEndOfLine(node.ValidNonAscii("λ"))) =
267    nibble.run(tokens, parser)
268}
269
270pub fn parse_not_end_of_line_rejects_newline_test() {
271  let input = "\n"
272  let tokens = quasi_lexer.chars() |> quasi_lexer.run(on: input)
273  let parser = parser.not_end_of_line()
274  let assert Error(_) = nibble.run(tokens, parser)
275}
276
277pub fn parse_not_end_of_line_rejects_carriage_return_test() {
278  let input = "\r"
279  let tokens = quasi_lexer.chars() |> quasi_lexer.run(on: input)
280  let parser = parser.not_end_of_line()
281  let assert Error(_) = nibble.run(tokens, parser)
282}