vq / vlib / v / builder / c_error_report.v
622 lines · 582 sloc · 17.58 KB · 26ed1f73bf37184b83bd2bb6de2f10f5b69b508e
Raw
1module builder
2
3import os
4import strings
5import v.pref
6import v.gen.c as cgen
7import v.util.version
8
9const default_c_error_bug_report_url = 'https://bugs.vlang.io/bug-report'
10const c_error_bug_report_disabled_env = 'V_C_ERROR_BUG_REPORT_DISABLED'
11const c_error_context_radius = 5
12const c_error_bug_report_max_body_bytes = 256 * 1024
13const c_error_bug_report_truncation_notice = '\n... report truncated before upload ...\n'
14
15struct CErrorReportLine {
16pub:
17 line int
18 text string
19}
20
21struct CErrorReportLocation {
22pub:
23 file string
24 line int
25}
26
27struct CErrorBugReport {
28pub:
29 kind string
30 v_version string
31 target_os string
32 target_backend string
33 ccompiler string
34 c_error string
35 c_file string
36 c_line int
37 c_context []CErrorReportLine
38 v_file string
39 v_line int
40 v_context []CErrorReportLine
41}
42
43fn (mut v Builder) submit_c_error_bug_report(ccompiler string, c_output string) {
44 if !should_submit_c_error_bug_report(v.pref.c_error_bug_report_url) {
45 return
46 }
47 mut raw_report := v.new_c_error_bug_report(ccompiler, c_output)
48 if raw_report.v_file == '' {
49 // The default `.tmp.c` has no `#line` directives, so the C error could not be
50 // traced back to a V line. Regenerate the C with `#line` info (as `-g` would),
51 // recompile, and reuse the richer report when it does map to a V source line.
52 if vlines_report := v.new_c_error_bug_report_with_vlines(ccompiler) {
53 raw_report = vlines_report
54 }
55 }
56 report := bounded_c_error_bug_report(raw_report, c_error_bug_report_max_body_bytes)
57 report_url := c_error_bug_report_url(v.pref.c_error_bug_report_url)
58 tool_output := send_c_error_bug_report(report, report_url) or {
59 eprintln('C compiler bug report was not sent to ${report_url}: ${err}')
60 return
61 }
62 println('================== C compiler bug report ==============')
63 if tool_output != '' {
64 println(tool_output)
65 }
66 print_c_error_bug_report_context(report)
67 println('='.repeat('================== C compiler bug report =============='.len))
68}
69
70fn (mut v Builder) new_c_error_bug_report(ccompiler string, c_output string) CErrorBugReport {
71 c_source := os.read_file(v.out_name_c) or { '' }
72 c_lines := c_source.split_into_lines()
73 mut c_file := v.out_name_c
74 mut c_line := 0
75 mut v_file := ''
76 mut v_line := 0
77 if c_loc := c_error_location_for_generated_c(c_output, v.out_name_c) {
78 c_file = c_loc.file
79 c_line = c_loc.line
80 if v_loc := v_source_location_for_c_line(c_lines, c_line, v.out_name_c) {
81 v_file = v_loc.file
82 v_line = v_loc.line
83 }
84 } else if source_loc := first_error_source_location(c_output) {
85 v_file = source_loc.file
86 v_line = source_loc.line
87 if found_c_line := generated_c_line_for_source_location(c_lines, source_loc, v.out_name_c) {
88 c_line = found_c_line
89 }
90 }
91 v_source := if v_file != '' { os.read_file(v_file) or { '' } } else { '' }
92 return CErrorBugReport{
93 kind: 'v-c-compiler-error'
94 v_version: version.full_v_version(true)
95 target_os: v.pref.os.str()
96 target_backend: v.pref.backend.str()
97 ccompiler: ccompiler
98 c_error: c_output
99 c_file: c_file
100 c_line: c_line
101 c_context: numbered_context_lines(c_lines, c_line, c_error_context_radius)
102 v_file: v_file
103 v_line: v_line
104 v_context: numbered_context_lines(v_source.split_into_lines(), v_line,
105 c_error_context_radius)
106 }
107}
108
109// new_c_error_bug_report_with_vlines regenerates the program's C source with `#line`
110// directives enabled (the same information `-g` would add), recompiles it with the
111// previously used C compiler command, and builds a report from the recompiled output.
112// Because the regenerated C carries `#line` annotations, the C error can be mapped back
113// to the exact V source line that produced it. It returns none when the V mapping still
114// cannot be produced, so the caller keeps the original, C-only report.
115fn (mut v Builder) new_c_error_bug_report_with_vlines(ccompiler string) ?CErrorBugReport {
116 if v.pref.is_vlines || v.pref.parallel_cc || v.pref.generate_c_project != ''
117 || v.last_cc_cmd == '' || v.parsed_files.len == 0 || v.out_name_c == '' {
118 return none
119 }
120 old_is_vlines := v.pref.is_vlines
121 v.pref.is_vlines = true
122 defer {
123 v.pref.is_vlines = old_is_vlines
124 }
125 // Regenerate the C source, now with `#line` directives, into the same `.tmp.c` file,
126 // so that the recorded compiler command recompiles exactly the annotated source.
127 // Keep the original `.tmp.c` so that it can be restored afterwards (e.g. for `-keepc`).
128 original_c := os.read_file(v.out_name_c) or { return none }
129 goutput := cgen.gen(v.parsed_files, mut v.table, v.pref)
130 mut c_builder := goutput.res_builder
131 c_builder = cgen.fix_reset_dbg_line(c_builder, v.out_name_c)
132 os.write_file_array(v.out_name_c, c_builder) or { return none }
133 vdir := os.dir(pref.vexe_path())
134 original_pwd := os.getwd()
135 os.chdir(vdir) or {}
136 recompiled := os.execute(v.last_cc_cmd)
137 os.chdir(original_pwd) or {}
138 report := v.new_c_error_bug_report(ccompiler, recompiled.output)
139 // Restore the C source that the user actually compiled, now that the report is built.
140 os.write_file(v.out_name_c, original_c) or {}
141 if report.v_file == '' {
142 return none
143 }
144 return report
145}
146
147fn c_error_bug_report_url(flag_url string) string {
148 trimmed_flag_url := flag_url.trim_space()
149 if trimmed_flag_url != '' {
150 return trimmed_flag_url.trim_right('/')
151 }
152 env_url := os.getenv('V_C_ERROR_BUG_REPORT_URL').trim_space()
153 if env_url != '' {
154 return env_url.trim_right('/')
155 }
156 return default_c_error_bug_report_url
157}
158
159fn should_submit_c_error_bug_report(flag_url string) bool {
160 if c_error_bug_reports_disabled() {
161 return false
162 }
163 if running_in_github_ci() {
164 return c_error_bug_report_url(flag_url) != default_c_error_bug_report_url
165 }
166 return true
167}
168
169fn c_error_bug_reports_disabled() bool {
170 return os.getenv(c_error_bug_report_disabled_env).trim_space().to_lower() in ['1', 'true',
171 'yes', 'on']
172}
173
174fn disable_c_error_bug_reports() {
175 os.setenv(c_error_bug_report_disabled_env, '1', true)
176}
177
178fn running_in_github_ci() bool {
179 return os.getenv('GITHUB_ACTIONS') == 'true' || os.getenv('GITHUB_JOB') != ''
180}
181
182fn send_c_error_bug_report(report CErrorBugReport, report_url string) !string {
183 report_path := os.join_path(os.vtmp_dir(), 'v-c-error-report-${os.getpid()}.json')
184 os.write_file(report_path, c_error_bug_report_json(report))!
185 defer {
186 os.rm(report_path) or {}
187 }
188 cmd := '${os.quoted_path(pref.vexe_path())} bug-report-send --url ${os.quoted_path(report_url)} --file ${os.quoted_path(report_path)}'
189 res := os.execute(cmd)
190 if res.exit_code != 0 {
191 return error(res.output.trim_space())
192 }
193 return res.output.trim_right('\r\n')
194}
195
196fn c_error_bug_report_json(report CErrorBugReport) string {
197 mut b := strings.new_builder(1024 + report.c_error.len)
198 b.write_u8(`{`)
199 write_json_string_field(mut b, 'kind', report.kind, false)
200 write_json_string_field(mut b, 'v_version', report.v_version, true)
201 write_json_string_field(mut b, 'target_os', report.target_os, true)
202 write_json_string_field(mut b, 'target_backend', report.target_backend, true)
203 write_json_string_field(mut b, 'ccompiler', report.ccompiler, true)
204 write_json_string_field(mut b, 'c_error', report.c_error, true)
205 write_json_string_field(mut b, 'c_file', report.c_file, true)
206 write_json_int_field(mut b, 'c_line', report.c_line, true)
207 write_json_report_lines_field(mut b, 'c_context', report.c_context, true)
208 write_json_string_field(mut b, 'v_file', report.v_file, true)
209 write_json_int_field(mut b, 'v_line', report.v_line, true)
210 write_json_report_lines_field(mut b, 'v_context', report.v_context, true)
211 b.write_u8(`}`)
212 return b.str()
213}
214
215fn write_json_string_field(mut b strings.Builder, name string, value string, needs_comma bool) {
216 write_json_field_name(mut b, name, needs_comma)
217 write_json_string(mut b, value)
218}
219
220fn write_json_int_field(mut b strings.Builder, name string, value int, needs_comma bool) {
221 write_json_field_name(mut b, name, needs_comma)
222 b.write_string(value.str())
223}
224
225fn write_json_report_lines_field(mut b strings.Builder, name string, lines []CErrorReportLine, needs_comma bool) {
226 write_json_field_name(mut b, name, needs_comma)
227 b.write_u8(`[`)
228 for idx, line in lines {
229 if idx > 0 {
230 b.write_u8(`,`)
231 }
232 b.write_u8(`{`)
233 write_json_int_field(mut b, 'line', line.line, false)
234 write_json_string_field(mut b, 'text', line.text, true)
235 b.write_u8(`}`)
236 }
237 b.write_u8(`]`)
238}
239
240fn write_json_field_name(mut b strings.Builder, name string, needs_comma bool) {
241 if needs_comma {
242 b.write_u8(`,`)
243 }
244 write_json_string(mut b, name)
245 b.write_u8(`:`)
246}
247
248fn write_json_string(mut b strings.Builder, value string) {
249 b.write_u8(`"`)
250 for ch in value.bytes() {
251 match ch {
252 `"` {
253 b.write_string('\\"')
254 }
255 `\\` {
256 b.write_string('\\\\')
257 }
258 `\b` {
259 b.write_string('\\b')
260 }
261 `\f` {
262 b.write_string('\\f')
263 }
264 `\n` {
265 b.write_string('\\n')
266 }
267 `\r` {
268 b.write_string('\\r')
269 }
270 `\t` {
271 b.write_string('\\t')
272 }
273 else {
274 if ch < 0x20 {
275 write_json_control_escape(mut b, ch)
276 } else {
277 b.write_u8(ch)
278 }
279 }
280 }
281 }
282 b.write_u8(`"`)
283}
284
285fn write_json_control_escape(mut b strings.Builder, ch u8) {
286 hex := '0123456789abcdef'
287 b.write_string('\\u00')
288 b.write_u8(hex[ch >> 4])
289 b.write_u8(hex[ch & 0x0f])
290}
291
292fn bounded_c_error_bug_report(report CErrorBugReport, max_body_bytes int) CErrorBugReport {
293 if max_body_bytes <= 0 || c_error_bug_report_json(report).len <= max_body_bytes {
294 return report
295 }
296 if bounded := report_with_bounded_c_error(report, max_body_bytes, report.c_context,
297 report.v_context)
298 {
299 return bounded
300 }
301 for context_text_bytes in [4096, 1024, 256, 80, 0] {
302 c_context := bounded_report_lines(report.c_context, context_text_bytes)
303 v_context := bounded_report_lines(report.v_context, context_text_bytes)
304 if bounded := report_with_bounded_c_error(report, max_body_bytes, c_context, v_context) {
305 return bounded
306 }
307 }
308 return CErrorBugReport{
309 ...report
310 c_error: truncated_report_text(report.c_error, 0)
311 c_context: []CErrorReportLine{}
312 v_context: []CErrorReportLine{}
313 }
314}
315
316fn report_with_bounded_c_error(report CErrorBugReport, max_body_bytes int, c_context []CErrorReportLine, v_context []CErrorReportLine) ?CErrorBugReport {
317 min_report := CErrorBugReport{
318 ...report
319 c_error: truncated_report_text(report.c_error, 0)
320 c_context: c_context
321 v_context: v_context
322 }
323 if c_error_bug_report_json(min_report).len > max_body_bytes {
324 return none
325 }
326 mut low := 0
327 mut high := report.c_error.len
328 mut best := min_report
329 for low <= high {
330 mid := (low + high) / 2
331 candidate := CErrorBugReport{
332 ...report
333 c_error: truncated_report_text(report.c_error, mid)
334 c_context: c_context
335 v_context: v_context
336 }
337 if c_error_bug_report_json(candidate).len <= max_body_bytes {
338 best = candidate
339 low = mid + 1
340 } else {
341 high = mid - 1
342 }
343 }
344 return best
345}
346
347fn bounded_report_lines(lines []CErrorReportLine, max_text_bytes int) []CErrorReportLine {
348 mut bounded := []CErrorReportLine{cap: lines.len}
349 for report_line in lines {
350 bounded << CErrorReportLine{
351 line: report_line.line
352 text: truncated_report_text(report_line.text, max_text_bytes)
353 }
354 }
355 return bounded
356}
357
358fn truncated_report_text(text string, max_bytes int) string {
359 if max_bytes <= 0 {
360 return ''
361 }
362 if text.len <= max_bytes {
363 return text
364 }
365 if max_bytes <= c_error_bug_report_truncation_notice.len {
366 return text[..max_bytes]
367 }
368 kept_bytes := max_bytes - c_error_bug_report_truncation_notice.len
369 head_bytes := kept_bytes / 2
370 tail_bytes := kept_bytes - head_bytes
371 return text[..head_bytes] + c_error_bug_report_truncation_notice + text[text.len - tail_bytes..]
372}
373
374fn print_c_error_bug_report_context(report CErrorBugReport) {
375 println('Generated C lines sent from ${report.c_file}:${report.c_line}:')
376 print_report_lines(report.c_context, report.c_line)
377 if report.v_file != '' {
378 println('Corresponding V lines sent from ${report.v_file}:${report.v_line}:')
379 print_report_lines(report.v_context, report.v_line)
380 } else {
381 println('Corresponding V lines sent: no V source mapping was available.')
382 }
383}
384
385fn print_report_lines(lines []CErrorReportLine, center int) {
386 if lines.len == 0 {
387 println(' (no source lines available)')
388 return
389 }
390 for line in lines {
391 prefix := if line.line == center { '>' } else { ' ' }
392 println('${prefix} ${line.line:6} | ${line.text}')
393 }
394}
395
396fn numbered_context_lines(lines []string, center int, radius int) []CErrorReportLine {
397 if center <= 0 || lines.len == 0 {
398 return []CErrorReportLine{}
399 }
400 mut start := center - radius
401 if start < 1 {
402 start = 1
403 }
404 mut end := center + radius
405 if end > lines.len {
406 end = lines.len
407 }
408 mut context := []CErrorReportLine{cap: end - start + 1}
409 for line_nr in start .. end + 1 {
410 context << CErrorReportLine{
411 line: line_nr
412 text: lines[line_nr - 1]
413 }
414 }
415 return context
416}
417
418fn c_error_location_for_generated_c(c_output string, generated_c_file string) ?CErrorReportLocation {
419 needles := c_error_generated_c_needles(generated_c_file)
420 for output_line in c_output.split_into_lines() {
421 if !output_line.to_lower_ascii().contains('error') {
422 continue
423 }
424 for needle in needles {
425 if loc := parse_error_location_after_needle(output_line, needle) {
426 return loc
427 }
428 }
429 }
430 return none
431}
432
433fn c_error_generated_c_needles(generated_c_file string) []string {
434 mut needles := []string{}
435 for candidate in [generated_c_file, os.real_path(generated_c_file),
436 os.file_name(generated_c_file)] {
437 if candidate != '' && candidate !in needles {
438 needles << candidate
439 }
440 normalized := candidate.replace('\\', '/')
441 if normalized != '' && normalized !in needles {
442 needles << normalized
443 }
444 }
445 return needles
446}
447
448fn parse_error_location_after_needle(output_line string, needle string) ?CErrorReportLocation {
449 idx := output_line.index(needle) or { return none }
450 after := output_line[idx + needle.len..]
451 if after.starts_with(':') {
452 line_nr := leading_int(after[1..])
453 if line_nr > 0 {
454 return CErrorReportLocation{
455 file: needle
456 line: line_nr
457 }
458 }
459 }
460 if after.starts_with('(') {
461 line_nr := leading_int(after[1..])
462 if line_nr > 0 {
463 return CErrorReportLocation{
464 file: needle
465 line: line_nr
466 }
467 }
468 }
469 return none
470}
471
472fn first_error_source_location(c_output string) ?CErrorReportLocation {
473 for output_line in c_output.split_into_lines() {
474 if !output_line.to_lower_ascii().contains('error') {
475 continue
476 }
477 if loc := parse_colon_error_location(output_line) {
478 return loc
479 }
480 if loc := parse_msvc_error_location(output_line) {
481 return loc
482 }
483 }
484 return none
485}
486
487fn parse_colon_error_location(output_line string) ?CErrorReportLocation {
488 parts := output_line.split(':')
489 if parts.len < 2 {
490 return none
491 }
492 for idx := 1; idx < parts.len; idx++ {
493 line_nr := parts[idx].int()
494 if line_nr <= 0 {
495 continue
496 }
497 file := parts[..idx].join(':')
498 if file == '' {
499 continue
500 }
501 return CErrorReportLocation{
502 file: file
503 line: line_nr
504 }
505 }
506 return none
507}
508
509fn parse_msvc_error_location(output_line string) ?CErrorReportLocation {
510 open_idx := output_line.index('(') or { return none }
511 close_rel_idx := output_line[open_idx + 1..].index(')') or { return none }
512 line_nr := leading_int(output_line[open_idx + 1..open_idx + 1 + close_rel_idx])
513 if line_nr <= 0 {
514 return none
515 }
516 return CErrorReportLocation{
517 file: output_line[..open_idx]
518 line: line_nr
519 }
520}
521
522fn v_source_location_for_c_line(c_lines []string, c_line int, generated_c_file string) ?CErrorReportLocation {
523 if c_line <= 0 || c_lines.len == 0 {
524 return none
525 }
526 mut current := CErrorReportLocation{}
527 last_line := if c_line <= c_lines.len { c_line } else { c_lines.len }
528 for idx in 0 .. last_line {
529 if directive := parse_line_directive(c_lines[idx]) {
530 current = directive
531 continue
532 }
533 if idx + 1 == c_line && is_v_source_file(current.file)
534 && !same_path(current.file, generated_c_file) {
535 return current
536 }
537 if current.file != '' {
538 current = CErrorReportLocation{
539 file: current.file
540 line: current.line + 1
541 }
542 }
543 }
544 return none
545}
546
547fn generated_c_line_for_source_location(c_lines []string, source CErrorReportLocation, generated_c_file string) ?int {
548 if source.file == '' || source.line <= 0 {
549 return none
550 }
551 mut current := CErrorReportLocation{}
552 mut fallback_line := 0
553 for idx, line in c_lines {
554 if directive := parse_line_directive(line) {
555 current = directive
556 continue
557 }
558 if is_v_source_file(current.file) && !same_path(current.file, generated_c_file)
559 && same_path(current.file, source.file) && current.line == source.line {
560 if fallback_line == 0 {
561 fallback_line = idx + 1
562 }
563 if line.trim_space() != '' {
564 return idx + 1
565 }
566 }
567 if current.file != '' {
568 current = CErrorReportLocation{
569 file: current.file
570 line: current.line + 1
571 }
572 }
573 }
574 if fallback_line > 0 {
575 return fallback_line
576 }
577 return none
578}
579
580fn parse_line_directive(line string) ?CErrorReportLocation {
581 trimmed := line.trim_space()
582 if !trimmed.starts_with('#line ') {
583 return none
584 }
585 rest := trimmed['#line '.len..].trim_space()
586 line_nr := leading_int(rest)
587 if line_nr <= 0 {
588 return none
589 }
590 first_quote_idx := rest.index('"') or { return none }
591 remaining := rest[first_quote_idx + 1..]
592 second_quote_idx := remaining.index('"') or { return none }
593 return CErrorReportLocation{
594 file: remaining[..second_quote_idx]
595 line: line_nr
596 }
597}
598
599fn leading_int(s string) int {
600 mut end := 0
601 for end < s.len && s[end].is_digit() {
602 end++
603 }
604 if end == 0 {
605 return 0
606 }
607 return s[..end].int()
608}
609
610fn is_v_source_file(path string) bool {
611 return path.ends_with('.v') || path.ends_with('.vv') || path.ends_with('.vsh')
612}
613
614fn same_path(a string, b string) bool {
615 if a == b {
616 return true
617 }
618 normalized_a := a.replace('\\', '/')
619 normalized_b := b.replace('\\', '/')
620 return normalized_a == normalized_b
621 || os.real_path(a).replace('\\', '/') == os.real_path(b).replace('\\', '/')
622}
623