1
use crate::errors::RecoverableParseError;
2
use crate::parser::traversal::WalkDFS;
3
use capitalize::Capitalize;
4
use std::collections::HashSet;
5
use tree_sitter::Node;
6

            
7
/// Returns the absolute byte offset of the start of `row` in `source`.
8
/// TODO: move to a separate module like utils, we don't want semantic tokens to depend on parser internals
9
125
pub fn line_start_byte(source: &[u8], row: usize) -> usize {
10
125
    let mut current_row = 0usize;
11
125
    let mut line_start = 0usize;
12
125
    for (idx, b) in source.iter().enumerate() {
13
1910
        if current_row == row {
14
125
            break;
15
1785
        }
16
1785
        if *b == b'\n' {
17
52
            current_row += 1;
18
52
            line_start = idx + 1;
19
1733
        }
20
    }
21
125
    line_start
22
125
}
23

            
24
73
fn point_range_at(source: &str, row: usize, column: usize) -> tree_sitter::Range {
25
73
    let line_start = line_start_byte(source.as_bytes(), row);
26
73
    let byte = line_start + column;
27
73
    tree_sitter::Range {
28
73
        start_byte: byte,
29
73
        end_byte: byte,
30
73
        start_point: tree_sitter::Point { row, column },
31
73
        end_point: tree_sitter::Point { row, column },
32
73
    }
33
73
}
34

            
35
77
fn is_int_keyword_suffix(prefix: &str) -> bool {
36
77
    let prefix = prefix.trim_end();
37
77
    if !prefix.ends_with("int") {
38
1
        return false;
39
76
    }
40
76
    let bytes = prefix.as_bytes();
41
76
    bytes.len() == 3 || {
42
76
        let b = bytes[bytes.len() - 4];
43
76
        !(b.is_ascii_alphanumeric() || b == b'_')
44
    }
45
77
}
46

            
47
896
fn int_domain_missing_rparen_line(line: &str, start_col: usize, end_col: usize) -> bool {
48
896
    line.as_bytes().get(start_col) == Some(&b'(')
49
76
        && line[end_col..].trim().is_empty()
50
75
        && !line[start_col..].contains(')')
51
74
        && is_int_keyword_suffix(&line[..start_col])
52
896
}
53

            
54
/// tree-sitter `ERROR` node spans can overlap bytes during recovery.
55
/// Need to clamp to the end of the non-comment prefix so diagnostics don't include comment
56
/// contents.
57
1086
fn clamp_range_before_line_comment(range: &mut tree_sitter::Range, source: &str) {
58
1086
    let Some(line) = source.lines().nth(range.start_point.row) else {
59
        return;
60
    };
61
1086
    let Some(dollar_idx) = line.find('$') else {
62
1037
        return;
63
    };
64

            
65
49
    let prefix = &line[..dollar_idx];
66
49
    let clamped_col = prefix.trim_end().len();
67

            
68
49
    if range.start_point.column > clamped_col {
69
        range.start_point.column = clamped_col;
70
49
    }
71
49
    if range.end_point.row == range.start_point.row && range.end_point.column > clamped_col {
72
1
        range.end_point.column = clamped_col;
73
48
    }
74
49
    if range.end_point.row > range.start_point.row {
75
        range.end_point.row = range.start_point.row;
76
        range.end_point.column = clamped_col;
77
49
    }
78

            
79
49
    let line_start = line_start_byte(source.as_bytes(), range.start_point.row);
80
49
    range.start_byte = line_start + range.start_point.column;
81
49
    range.end_byte = line_start + range.end_point.column;
82
1086
}
83

            
84
1305
pub fn detect_syntactic_errors(
85
1305
    source: &str,
86
1305
    tree: &tree_sitter::Tree,
87
1305
    errors: &mut Vec<RecoverableParseError>,
88
1305
) {
89
1305
    let mut malformed_lines_reported = HashSet::new();
90

            
91
1305
    let root_node = tree.root_node();
92
33565
    let retract: &dyn Fn(&tree_sitter::Node) -> bool = &|node: &tree_sitter::Node| {
93
33565
        node.is_missing() || node.is_error() || node.start_position() == node.end_position()
94
33565
    };
95

            
96
1305
    for node in WalkDFS::with_retract(&root_node, &retract) {
97
33565
        if node.start_position() == node.end_position() {
98
265
            errors.push(classify_missing_token(node, source));
99
265
            continue;
100
33300
        }
101
33300
        if node.is_error() {
102
1208
            let line = node.start_position().row;
103
            // If this line has already been reported as malformed, skip all error nodes on this line
104
1208
            if malformed_lines_reported.contains(&line) {
105
                continue;
106
1208
            }
107
            // Ignore error nodes that start inside a single-line comment.
108
1208
            if let Some(line_str) = source.lines().nth(line)
109
1208
                && let Some(dollar_idx) = line_str.find('$')
110
96
                && node.start_position().column >= dollar_idx
111
            {
112
                continue;
113
1208
            }
114

            
115
1208
            if let Some(missing_expression) = classify_such_that_missing_expression(&node, source) {
116
                errors.push(missing_expression);
117
                continue;
118
1208
            }
119

            
120
1208
            if is_malformed_line_error(&node, source) {
121
316
                malformed_lines_reported.insert(line);
122
316
                let start_byte = node.start_byte();
123
316
                let end_byte = node.end_byte();
124

            
125
316
                let last_char = source.lines().nth(line).map_or(0, |l| l.len());
126
316
                errors.push(RecoverableParseError::new(
127
316
                    generate_malformed_line_message(line, source),
128
316
                    Some(tree_sitter::Range {
129
316
                        start_byte,
130
316
                        end_byte,
131
316
                        start_point: tree_sitter::Point {
132
316
                            row: line,
133
316
                            column: 0,
134
316
                        },
135
316
                        end_point: tree_sitter::Point {
136
316
                            row: line,
137
316
                            column: last_char,
138
316
                        },
139
316
                    }),
140
316
                ));
141
316
                continue;
142
            } else {
143
892
                if let Some(missing_rparen) = classify_int_domain_missing_rparen(&node, source) {
144
72
                    errors.push(missing_rparen);
145
72
                    continue;
146
820
                }
147
820
                errors.push(classify_unexpected_token_error(node, source));
148
            }
149
820
            continue;
150
32092
        }
151
    }
152
1305
}
153

            
154
/// Tree-sitter recovery sometimes reduces `int_domain` to bare `int` and then wraps the following
155
/// `(` and range text in an `ERROR` node (especially at EOF).
156
/// This function detects this specific pattern and reports  "Missing )" error
157
892
fn classify_int_domain_missing_rparen(
158
892
    node: &tree_sitter::Node,
159
892
    source: &str,
160
892
) -> Option<RecoverableParseError> {
161
892
    let start = node.start_position();
162
892
    let end = node.end_position();
163
892
    let line = source.lines().nth(start.row)?;
164
892
    let comment_col = line.find('$').unwrap_or(line.len());
165
892
    let line = &line[..comment_col];
166
892
    let start_col = start.column.min(line.len());
167
892
    let end_col = end.column.min(line.len());
168
892
    if !int_domain_missing_rparen_line(line, start_col, end_col) {
169
820
        return None;
170
72
    }
171
72
    let insertion_col = line.trim_end().len();
172
72
    Some(RecoverableParseError::new(
173
72
        "Missing )".to_string(),
174
72
        Some(point_range_at(source, start.row, insertion_col)),
175
72
    ))
176
892
}
177

            
178
1208
fn classify_such_that_missing_expression(
179
1208
    node: &tree_sitter::Node,
180
1208
    source: &str,
181
1208
) -> Option<RecoverableParseError> {
182
1208
    let line = source.lines().nth(node.start_position().row)?;
183
1208
    if source[node.start_byte()..node.end_byte()].trim() != "such that" {
184
1208
        return None;
185
    }
186
    Some(RecoverableParseError::new(
187
        "Missing Expression".to_string(),
188
        Some(point_range_at(
189
            source,
190
            node.start_position().row,
191
            line.trim_end().len(),
192
        )),
193
    ))
194
1208
}
195

            
196
/// Classifies a missing token node and generates a diagnostic with a context-aware message.
197
265
fn classify_missing_token(node: Node, source: &str) -> RecoverableParseError {
198
265
    let mut range = tree_sitter::Range {
199
265
        start_byte: node.start_byte(),
200
265
        end_byte: node.end_byte(),
201
265
        start_point: node.start_position(),
202
265
        end_point: node.end_position(),
203
265
    };
204
265
    clamp_range_before_line_comment(&mut range, source);
205

            
206
265
    let message = if let Some(parent) = node.parent() {
207
265
        match parent.kind() {
208
48
            "letting_variable_declaration" => "Missing Expression or Domain".to_string(),
209
217
            _ => format!("Missing {}", user_friendly_token_name(node.kind(), false)),
210
        }
211
    } else {
212
        format!("Missing {}", user_friendly_token_name(node.kind(), false))
213
    };
214

            
215
265
    RecoverableParseError::new(message, Some(range))
216
265
}
217

            
218
/// Classifies an unexpected token error node and generates a diagnostic.
219
820
fn classify_unexpected_token_error(node: Node, source_code: &str) -> RecoverableParseError {
220
820
    let mut range = tree_sitter::Range {
221
820
        start_byte: node.start_byte().min(source_code.len()),
222
820
        end_byte: node.end_byte().min(source_code.len()),
223
820
        start_point: node.start_position(),
224
820
        end_point: node.end_position(),
225
820
    };
226
820
    clamp_range_before_line_comment(&mut range, source_code);
227

            
228
820
    let message = if let Some(parent) = node.parent() {
229
        // Extract the unexpected token text, handling out-of-range indices safely.
230
        // NOTE: tree-sitter byte offsets can land inside UTF-8 codepoints; decoding lossily avoids panics.
231
820
        let src_token: std::borrow::Cow<'_, str> = source_code
232
820
            .as_bytes()
233
820
            .get(range.start_byte..range.end_byte)
234
820
            .map(String::from_utf8_lossy)
235
            .unwrap_or_else(|| std::borrow::Cow::Borrowed("<unknown>"));
236
820
        let src_token = src_token.trim_end();
237

            
238
820
        if parent.kind() == "program" {
239
292
            format!("Unexpected {}", src_token)
240
        } else {
241
528
            format!(
242
                "Unexpected {} inside {}",
243
528
                src_token,
244
528
                user_friendly_token_name(parent.kind(), true)
245
            )
246
        }
247
    } else {
248
        "Unexpected token".to_string()
249
    };
250

            
251
820
    RecoverableParseError::new(message, Some(range))
252
820
}
253

            
254
/// Determines if an error node represents a malformed line error.
255
1209
pub fn is_malformed_line_error(node: &tree_sitter::Node, source: &str) -> bool {
256
1209
    let parent = node.parent();
257
1209
    let grandparent = parent.and_then(|n| n.parent());
258
1209
    let root = grandparent.and_then(|n| n.parent());
259

            
260
1209
    if let (Some(parent), Some(grandparent), Some(root)) = (parent, grandparent, root)
261
504
        && parent.kind() == "set_comparison"
262
        && grandparent.kind() == "comparison_expr"
263
        && root.kind() == "program"
264
    {
265
        return true;
266
1209
    }
267

            
268
    // check parent kinds to see if the error is a constraint continuation
269
1209
    let mut curr = node.parent();
270
2778
    while let Some(n) = curr {
271
2097
        let kind = n.kind();
272
2097
        if matches!(
273
2097
            kind,
274
            "find_statement"
275
                | "given_statement"
276
                | "letting_statement"
277
                | "dominance_relation"
278
                | "bool_expr"
279
                | "comparison_expr"
280
                | "arithmetic_expr"
281
                | "atom"
282
        ) {
283
528
            return false;
284
1569
        }
285
1569
        curr = n.parent();
286
    }
287

            
288
    // check for the first non-whitespace character on the line before the error node
289
681
    let line = source.lines().nth(node.start_position().row).unwrap_or("");
290
681
    let first_non_witespace = line
291
681
        .as_bytes()
292
681
        .iter()
293
706
        .take_while(|b| b.is_ascii_whitespace())
294
        .count();
295

            
296
    // if the error node is before or at the first non-whitespace character, it's a malformed line error
297
    // if the first non-whitespace character is after the error node, it could be a constraint continuation
298
681
    if node.start_position().column <= first_non_witespace || error_node_out_of_range(node, source)
299
    {
300
317
        if first_non_witespace > 0 && is_constraint_continuation(source, node.start_position().row)
301
        {
302
            return false;
303
317
        }
304
317
        return true;
305
364
    }
306
364
    false
307
1209
}
308

            
309
/// Checks if a line is a continuation of a constraint (i.e., it ends with a comma or has "such that" at the start).
310
25
fn is_constraint_continuation(source: &str, row: usize) -> bool {
311
25
    let lines: Vec<&str> = source.lines().collect();
312
25
    if row == 0 {
313
25
        return false;
314
    }
315

            
316
    let mut r = row;
317
    while r > 0 {
318
        r -= 1;
319
        let line = lines.get(r).copied().unwrap_or("");
320
        let line = line.split('$').next().unwrap_or("").trim_end();
321
        if line.trim().is_empty() {
322
            continue;
323
        }
324
        let lower = line.trim_start().to_ascii_lowercase();
325
        return lower.starts_with("such that") || line.ends_with(',');
326
    }
327
    false
328
25
}
329

            
330
/// Coverts a token name into a more user-friendly format for error messages.
331
/// Removes underscores, replaces certain keywords with more natural language, and adds appropriate articles.
332
748
fn user_friendly_token_name(token: &str, article: bool) -> String {
333
748
    let capitalized = if token.contains("atom") {
334
96
        "Expression".to_string()
335
652
    } else if token == "COLON" {
336
49
        ":".to_string()
337
    } else {
338
603
        let friendly_name = token
339
603
            .replace("literal", "")
340
603
            .replace("int", "Integer")
341
603
            .replace("expr", "Expression")
342
603
            .replace('_', " ");
343
603
        friendly_name
344
603
            .split_whitespace()
345
1013
            .map(|word| word.capitalize())
346
            .collect::<Vec<_>>()
347
603
            .join(" ")
348
    };
349

            
350
748
    if !article {
351
219
        return capitalized;
352
529
    }
353
529
    let first_char = capitalized.chars().next().unwrap();
354
529
    let article = match first_char.to_ascii_lowercase() {
355
217
        'a' | 'e' | 'i' | 'o' | 'u' => "an",
356
312
        _ => "a",
357
    };
358
529
    format!("{} {}", article, capitalized)
359
748
}
360

            
361
// Generates an informative error message for malformed lines
362
324
fn generate_malformed_line_message(line: usize, source: &str) -> String {
363
324
    let got = source.lines().nth(line).unwrap_or("").trim();
364
324
    let got = got.split('$').next().unwrap_or("").trim_end();
365
324
    let got = got.replace('"', "\\\"");
366
324
    let mut words = got.split_whitespace();
367
324
    let first = words.next().unwrap_or("").to_ascii_lowercase();
368
324
    let second = words.next().unwrap_or("").to_ascii_lowercase();
369

            
370
324
    let expected = match first.as_str() {
371
173
        "find" => "a find declaration statement",
372
        "findAux" => "a findAux declaration statement",
373
25
        "letting" => "a letting declaration statement",
374
1
        "given" => "a given declaration statement",
375
1
        "where" => "an instantiation condition",
376
1
        "minimising" | "maximising" => "an objective statement",
377
        // Check for invalid constraint statement
378
26
        "such" if second == "that" => "a constraint statement",
379
1
        "such" => "a valid top-level statement",
380
        _ => {
381
            // Default case for unrecognized starting tokens
382
97
            "a valid top-level statement"
383
        }
384
    };
385
324
    format!("Expected {}, but got '{}'", expected, got)
386
324
}
387

            
388
/// Returns true if the node's start or end column is out of range for its line in the source.
389
364
fn error_node_out_of_range(node: &tree_sitter::Node, source: &str) -> bool {
390
364
    let lines: Vec<&str> = source.lines().collect();
391
364
    let start = node.start_position();
392
364
    let end = node.end_position();
393

            
394
364
    let start_line_len = lines.get(start.row).map_or(0, |l| l.len());
395
364
    let end_line_len = lines.get(end.row).map_or(0, |l| l.len());
396

            
397
364
    (start.column > start_line_len) || (end.column > end_line_len)
398
364
}
399

            
400
#[cfg(test)]
401
mod test {
402

            
403
    use super::{
404
        clamp_range_before_line_comment, detect_syntactic_errors, int_domain_missing_rparen_line,
405
        is_int_keyword_suffix, is_malformed_line_error, line_start_byte, point_range_at,
406
        user_friendly_token_name,
407
    };
408
    use crate::errors::RecoverableParseError;
409
    use crate::{parser::traversal::WalkDFS, util::get_tree};
410

            
411
    /// Helper function for tests to compare the actual error with the expected one.
412
1
    fn assert_essence_parse_error_eq(a: &RecoverableParseError, b: &RecoverableParseError) {
413
1
        assert_eq!(a.msg, b.msg, "error messages differ");
414
1
        assert_eq!(a.range, b.range, "error ranges differ");
415
1
    }
416

            
417
    #[test]
418
1
    fn malformed_line() {
419
1
        let source = " a,a,b: int(1..3)";
420
1
        let (tree, _) = get_tree(source).expect("Should parse");
421
1
        let root_node = tree.root_node();
422

            
423
1
        let error_node = WalkDFS::with_retract(&root_node, &|_node| false)
424
2
            .find(|node| node.is_error())
425
1
            .expect("Should find an error node");
426

            
427
1
        assert!(is_malformed_line_error(&error_node, source));
428
1
    }
429

            
430
    #[test]
431
1
    fn malformed_find_message() {
432
1
        let source = "find >=lex,b,c: int(1..3)";
433
1
        let message = super::generate_malformed_line_message(0, source);
434
1
        assert_eq!(
435
1
            message,
436
1
            "Expected a find declaration statement, but got 'find >=lex,b,c: int(1..3)'"
437
        );
438
1
    }
439

            
440
    #[test]
441
1
    fn malformed_top_level_message() {
442
1
        let source = "a >=lex,b,c: int(1..3)";
443
1
        let message = super::generate_malformed_line_message(0, source);
444
1
        assert_eq!(
445
1
            message,
446
1
            "Expected a valid top-level statement, but got 'a >=lex,b,c: int(1..3)'"
447
        );
448
1
    }
449

            
450
    #[test]
451
1
    fn malformed_objective_message() {
452
1
        let source = "maximising %x";
453
1
        let message = super::generate_malformed_line_message(0, source);
454
1
        assert_eq!(
455
1
            message,
456
1
            "Expected an objective statement, but got 'maximising %x'"
457
        );
458
1
    }
459

            
460
    #[test]
461
1
    fn malformed_letting_message() {
462
1
        let source = "letting % A be 3";
463
1
        let message = super::generate_malformed_line_message(0, source);
464
1
        assert_eq!(
465
1
            message,
466
1
            "Expected a letting declaration statement, but got 'letting % A be 3'"
467
        );
468
1
    }
469

            
470
    #[test]
471
1
    fn malformed_constraint_message() {
472
1
        let source = "such that % A > 3";
473
1
        let message = super::generate_malformed_line_message(0, source);
474
1
        assert_eq!(
475
1
            message,
476
1
            "Expected a constraint statement, but got 'such that % A > 3'"
477
        );
478
1
    }
479

            
480
    #[test]
481
1
    fn malformed_top_level_message_2() {
482
1
        let source = "such % A > 3";
483
1
        let message = super::generate_malformed_line_message(0, source);
484
1
        assert_eq!(
485
1
            message,
486
1
            "Expected a valid top-level statement, but got 'such % A > 3'"
487
        );
488
1
    }
489

            
490
    #[test]
491
1
    fn malformed_given_message() {
492
1
        let source = "given 1..3)";
493
1
        let message = super::generate_malformed_line_message(0, source);
494
1
        assert_eq!(
495
1
            message,
496
1
            "Expected a given declaration statement, but got 'given 1..3)'"
497
        );
498
1
    }
499

            
500
    #[test]
501
1
    fn malformed_where_message() {
502
1
        let source = "where x>6";
503
1
        let message = super::generate_malformed_line_message(0, source);
504
1
        assert_eq!(
505
1
            message,
506
1
            "Expected an instantiation condition, but got 'where x>6'"
507
        );
508
1
    }
509

            
510
    #[test]
511
1
    fn user_friendly_token_name_article() {
512
1
        assert_eq!(
513
1
            user_friendly_token_name("int_domain", false),
514
1
            "Integer Domain"
515
        );
516
1
        assert_eq!(
517
1
            user_friendly_token_name("int_domain", true),
518
1
            "an Integer Domain"
519
        );
520
        // assert_eq!(user_friendly_token_name("atom", true), "an Expression");
521
1
        assert_eq!(user_friendly_token_name("COLON", false), ":");
522
1
    }
523

            
524
    #[test]
525
1
    fn missing_domain() {
526
1
        let source = "find x:";
527
1
        let (tree, _) = get_tree(source).expect("Should parse");
528
1
        let mut errors = vec![];
529
1
        detect_syntactic_errors(source, &tree, &mut errors);
530
1
        assert_eq!(errors.len(), 1, "Expected exactly one diagnostic");
531

            
532
1
        let error = &errors[0];
533

            
534
1
        assert_essence_parse_error_eq(
535
1
            error,
536
1
            &RecoverableParseError::new(
537
1
                "Missing Domain".to_string(),
538
1
                Some(tree_sitter::Range {
539
1
                    start_byte: 7,
540
1
                    end_byte: 7,
541
1
                    start_point: tree_sitter::Point { row: 0, column: 7 },
542
1
                    end_point: tree_sitter::Point { row: 0, column: 7 },
543
1
                }),
544
1
            ),
545
1
        );
546
1
    }
547

            
548
    #[test]
549
1
    fn line_start_byte_returns_correct_offsets() {
550
1
        let source = "a\nbc\ndef";
551
1
        let bytes = source.as_bytes();
552
1
        assert_eq!(line_start_byte(bytes, 0), 0);
553
1
        assert_eq!(line_start_byte(bytes, 1), 2);
554
1
        assert_eq!(line_start_byte(bytes, 2), 5);
555
1
    }
556

            
557
    #[test]
558
1
    fn point_range_at_returns_correct_zero_length_range() {
559
1
        let source = "a\nbc\ndef";
560
1
        let range = point_range_at(source, 1, 1); // points to 'c'
561
1
        assert_eq!(range.start_point.row, 1);
562
1
        assert_eq!(range.start_point.column, 1);
563
1
        assert_eq!(range.end_point, range.start_point);
564
1
        assert_eq!(range.start_byte, 3);
565
1
        assert_eq!(range.end_byte, 3);
566
1
    }
567

            
568
    #[test]
569
1
    fn clamp_range_before_line_comment_clamps_end_to_before_dollar() {
570
1
        let source = "find x: int(1..3 $comment";
571
1
        let mut range = tree_sitter::Range {
572
1
            start_byte: 0,
573
1
            end_byte: source.len(),
574
1
            start_point: tree_sitter::Point { row: 0, column: 0 },
575
1
            end_point: tree_sitter::Point {
576
1
                row: 0,
577
1
                column: source.len(),
578
1
            },
579
1
        };
580

            
581
1
        clamp_range_before_line_comment(&mut range, source);
582

            
583
        // "find x: int(1..3" ends at byte/column 16; the `$comment` suffix must be excluded.
584
1
        assert_eq!(range.end_point.row, 0);
585
1
        assert_eq!(range.end_point.column, 16);
586
1
        assert_eq!(range.end_byte, 16);
587
1
    }
588

            
589
    #[test]
590
1
    fn int_keyword_suffix_checks_word_boundary() {
591
1
        assert!(is_int_keyword_suffix("find x: int"));
592
1
        assert!(!is_int_keyword_suffix("foo"));
593
1
        assert!(!is_int_keyword_suffix("mint"));
594
1
    }
595

            
596
    #[test]
597
1
    fn int_domain_missing_rparen_line_positive_and_negative_cases() {
598
1
        let ok = "find x: int(1..2";
599
1
        let start = ok.find('(').unwrap();
600
1
        assert!(int_domain_missing_rparen_line(ok, start, ok.len()));
601

            
602
1
        let has_rparen = "find x: int(1..2)";
603
1
        let start = has_rparen.find('(').unwrap();
604
1
        assert!(!int_domain_missing_rparen_line(
605
1
            has_rparen,
606
1
            start,
607
1
            has_rparen.len()
608
1
        ));
609

            
610
1
        let trailing = "find x: int(1..2 foo";
611
1
        let start = trailing.find('(').unwrap();
612
1
        let end = trailing.find(" foo").unwrap();
613
1
        assert!(!int_domain_missing_rparen_line(trailing, start, end));
614

            
615
1
        let print_like = "find x: print(1..2";
616
1
        let start = print_like.find('(').unwrap();
617
1
        assert!(!int_domain_missing_rparen_line(
618
1
            print_like,
619
1
            start,
620
1
            print_like.len()
621
1
        ));
622
1
    }
623
}