|
| 1 | +module errors |
| 2 | + |
| 3 | +import encoding.utf8.east_asian |
| 4 | +import os |
| 5 | +import strings |
| 6 | +import v3.flat |
| 7 | +import v3.token |
| 8 | + |
| 9 | +const source_context_before = 2 |
| 10 | +const source_context_after = 2 |
| 11 | + |
| 12 | +// formatted_error renders a compiler diagnostic with v1-compatible source context. |
| 13 | +pub fn formatted_error(kind string, message string, a &flat.FlatAst, node flat.NodeId, pos token.Pos) string { |
| 14 | + if pos.is_valid() { |
| 15 | + file := a.source_files[pos.id] or { return '${kind} ${message}' } |
| 16 | + return formatted_source_error(kind, message, file, pos) |
| 17 | + } |
| 18 | + if int(node) < 0 || int(node) >= a.nodes.len { |
| 19 | + return '${kind} ${message}' |
| 20 | + } |
| 21 | + n := a.nodes[int(node)] |
| 22 | + file := a.source_files[n.pos.id] or { return '${kind} ${message}' } |
| 23 | + return formatted_source_error(kind, message, file, n.pos) |
| 24 | +} |
| 25 | + |
| 26 | +// formatted_source_error renders a diagnostic for a source file and byte span. |
| 27 | +pub fn formatted_source_error(kind string, message string, file &token.File, pos token.Pos) string { |
| 28 | + position := file.position(pos) |
| 29 | + path := relative_error_path(file.name) |
| 30 | + mut result := strings.new_builder(message.len + 256) |
| 31 | + reported_column := if pos.reported_column > 0 { |
| 32 | + pos.reported_column |
| 33 | + } else { |
| 34 | + position.column |
| 35 | + } |
| 36 | + result.writeln('${path}:${position.line}:${reported_column}: ${kind} ${message}') |
| 37 | + source := os.read_file(file.name) or { return result.str().trim_right('\n') } |
| 38 | + lines := source.split_into_lines() |
| 39 | + if lines.len == 0 { |
| 40 | + return result.str().trim_right('\n') |
| 41 | + } |
| 42 | + first_line := int_max(1, position.line - source_context_before) |
| 43 | + last_line := int_min(lines.len, position.line + source_context_after) |
| 44 | + for line_number := first_line; line_number <= last_line; line_number++ { |
| 45 | + line := lines[line_number - 1] |
| 46 | + result.writeln('${line_number:5d} | ${line.replace('\t', ' ')}') |
| 47 | + if line_number == position.line { |
| 48 | + line_start := file.line_start(position.line) |
| 49 | + start_byte := int_max(0, int_min(pos.offset - line_start, line.len)) |
| 50 | + span_end := int_max(pos.offset + 1, pos.end) |
| 51 | + end_byte := int_min(line.len, int_max(start_byte + 1, int_min(span_end - line_start, |
| 52 | + line.len))) |
| 53 | + mut pointer := strings.new_builder(line.len + 8) |
| 54 | + prefix := line[..start_byte].replace('\t', ' ') |
| 55 | + pointer.write_string(' '.repeat(diagnostic_display_width(prefix))) |
| 56 | + underline_len := int_max(1, diagnostic_display_width(line[start_byte..end_byte])) |
| 57 | + pointer.write_string(if underline_len > 1 { |
| 58 | + '~'.repeat(underline_len) |
| 59 | + } else { |
| 60 | + '^' |
| 61 | + }) |
| 62 | + result.writeln(' | ${pointer.str().replace('\t', ' ')}') |
| 63 | + } |
| 64 | + } |
| 65 | + return result.str().trim_right('\n') |
| 66 | +} |
| 67 | + |
| 68 | +fn diagnostic_display_width(text string) int { |
| 69 | + mut width := 0 |
| 70 | + mut valid_start := 0 |
| 71 | + mut i := 0 |
| 72 | + for i < text.len { |
| 73 | + sequence_len := valid_utf8_sequence_len(text, i) |
| 74 | + if sequence_len > 0 { |
| 75 | + i += sequence_len |
| 76 | + continue |
| 77 | + } |
| 78 | + if valid_start < i { |
| 79 | + width += east_asian.display_width(text[valid_start..i], 1) |
| 80 | + } |
| 81 | + width++ |
| 82 | + i++ |
| 83 | + valid_start = i |
| 84 | + } |
| 85 | + if valid_start < text.len { |
| 86 | + width += east_asian.display_width(text[valid_start..], 1) |
| 87 | + } |
| 88 | + return width |
| 89 | +} |
| 90 | + |
| 91 | +fn valid_utf8_sequence_len(text string, index int) int { |
| 92 | + first := text[index] |
| 93 | + if first < 0x80 { |
| 94 | + return 1 |
| 95 | + } |
| 96 | + mut length := 0 |
| 97 | + mut second_min := u8(0x80) |
| 98 | + mut second_max := u8(0xbf) |
| 99 | + if first >= 0xc2 && first <= 0xdf { |
| 100 | + length = 2 |
| 101 | + } else if first >= 0xe0 && first <= 0xef { |
| 102 | + length = 3 |
| 103 | + if first == 0xe0 { |
| 104 | + second_min = 0xa0 |
| 105 | + } else if first == 0xed { |
| 106 | + second_max = 0x9f |
| 107 | + } |
| 108 | + } else if first >= 0xf0 && first <= 0xf4 { |
| 109 | + length = 4 |
| 110 | + if first == 0xf0 { |
| 111 | + second_min = 0x90 |
| 112 | + } else if first == 0xf4 { |
| 113 | + second_max = 0x8f |
| 114 | + } |
| 115 | + } else { |
| 116 | + return 0 |
| 117 | + } |
| 118 | + if index + length > text.len || text[index + 1] < second_min || text[index + 1] > second_max { |
| 119 | + return 0 |
| 120 | + } |
| 121 | + for i in index + 2 .. index + length { |
| 122 | + if text[i] < 0x80 || text[i] > 0xbf { |
| 123 | + return 0 |
| 124 | + } |
| 125 | + } |
| 126 | + return length |
| 127 | +} |
| 128 | + |
| 129 | +fn relative_error_path(path string) string { |
| 130 | + mut normalized := os.real_path(path).replace('\\', '/') |
| 131 | + if os.getenv('VERROR_PATHS') == 'absolute' { |
| 132 | + return normalized |
| 133 | + } |
| 134 | + workdir := os.getwd().replace('\\', '/').trim_right('/') + '/' |
| 135 | + if normalized.starts_with(workdir) { |
| 136 | + normalized = normalized[workdir.len..] |
| 137 | + } |
| 138 | + return normalized |
| 139 | +} |
0 commit comments