Skip to content

Commit 08b51e8

Browse files
committed
v3: snapshot generated-C report sources
1 parent 6767c91 commit 08b51e8

12 files changed

Lines changed: 215 additions & 42 deletions

File tree

cmd/v/macos_v3_dispatch.c.v

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ const macos_v3_c_error_compiler_file = 'compiler'
3737
const macos_v3_c_error_output_file = 'output'
3838
const macos_v3_c_error_source_name_file = 'source_name'
3939
const macos_v3_c_error_v_sources_file = 'v_sources'
40+
const macos_v3_c_error_v_source_digests_file = 'v_source_digests'
4041
// optional marker file inside a staged report dir; when it holds
4142
// `macos_v3_compiler_error_fallback` the report describes a V3 internal compiler
4243
// error (V source only) instead of a generated-C compilation error.
@@ -314,7 +315,8 @@ fn retry_macos_v3_with_old_compiler(caller_environment map[string]string, fallba
314315
// filing a report. Still forward a notice-only marker so that once the stable
315316
// build succeeds the user sees the documented fallback notice (doc/docs.md) rather
316317
// than a silent switch that is indistinguishable from a direct V3 success.
317-
export_macos_v3_report_content(macos_v3_inline_asm_fallback, 'v3', '', '', [])
318+
export_macos_v3_report_content(macos_v3_inline_asm_fallback, 'v3', '', '',
319+
map[string]string{})
318320
os.rmdir_all(c_error_dir) or {}
319321
if should_report {
320322
eprintln('V3 requested the compatibility compiler for inline assembly')
@@ -349,7 +351,7 @@ fn take_macos_v3_report_content() ?MacosV3CErrorReport {
349351
// export_macos_v3_report_content bounds the fallback source in THIS process — which staged
350352
// the report and therefore trusts `c_file` — and forwards only that content to the V1
351353
// retry through the environment.
352-
fn export_macos_v3_report_content(kind string, ccompiler string, c_output string, c_file string, v_sources []string) {
354+
fn export_macos_v3_report_content(kind string, ccompiler string, c_output string, c_file string, v_sources map[string]string) {
353355
v_file, v_source := builder.bounded_v3_fallback_source(kind, c_output, c_file, v_sources)
354356
export_macos_v3_bounded_report_content(kind, ccompiler, c_output, v_file, v_source)
355357
}
@@ -426,7 +428,7 @@ struct MacosV3StagedReport {
426428
ccompiler string
427429
c_output string
428430
c_file string
429-
v_sources []string
431+
v_sources map[string]string
430432
report_dir string
431433
}
432434

@@ -462,7 +464,21 @@ fn read_macos_v3_c_error_report(report_dir string) ?MacosV3StagedReport {
462464
v_sources_text := os.read_file(os.join_path(report_dir, macos_v3_c_error_v_sources_file)) or {
463465
return none
464466
}
465-
v_sources := v_sources_text.split('\x00').filter(it != '')
467+
v_source_digests_text := os.read_file(os.join_path(report_dir,
468+
macos_v3_c_error_v_source_digests_file)) or { return none }
469+
v_source_paths := v_sources_text.split('\x00').filter(it != '')
470+
v_source_digests := v_source_digests_text.split('\x00').filter(it != '')
471+
if v_source_paths.len != v_source_digests.len {
472+
return none
473+
}
474+
mut v_sources := map[string]string{}
475+
for i, path in v_source_paths {
476+
digest := v_source_digests[i]
477+
if digest.len != sha256.size * 2 {
478+
return none
479+
}
480+
v_sources[path] = digest
481+
}
466482
return MacosV3StagedReport{
467483
kind: kind
468484
ccompiler: ccompiler.trim_space()

cmd/v/macos_v3_test.v

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
module main
22

33
import os
4+
import crypto.sha256
45
import v.pref
56
import v.builder
67

@@ -1545,7 +1546,10 @@ fn test_macos_v3_reads_c_error_fallback_report() {
15451546
os.write_file(os.join_path(root, macos_v3_c_error_compiler_file), 'clang')!
15461547
os.write_file(os.join_path(root, macos_v3_c_error_output_file),
15471548
'src.c:2:1: error: generated failure')!
1548-
os.write_file(os.join_path(root, macos_v3_c_error_v_sources_file), '')!
1549+
parsed_source := os.join_path(root, 'parsed.v')
1550+
parsed_digest := 'a'.repeat(sha256.size * 2)
1551+
os.write_file(os.join_path(root, macos_v3_c_error_v_sources_file), parsed_source)!
1552+
os.write_file(os.join_path(root, macos_v3_c_error_v_source_digests_file), parsed_digest)!
15491553
os.write_file(os.join_path(root, 'src.c'), 'int main(void) { return missing; }\n')!
15501554
report := read_macos_v3_c_error_report(root) or {
15511555
assert false
@@ -1554,6 +1558,7 @@ fn test_macos_v3_reads_c_error_fallback_report() {
15541558
assert report.ccompiler == 'clang'
15551559
assert report.c_output.contains('generated failure')
15561560
assert report.c_file == os.join_path(root, 'src.c')
1561+
assert report.v_sources[parsed_source] == parsed_digest
15571562
assert report.report_dir == root
15581563
}
15591564
}
@@ -2014,7 +2019,7 @@ fn test_take_macos_v3_report_content_carries_no_path() {
20142019
// A forwarded content report round-trips as content only.
20152020
compiler_error := macos_v3_compiler_error_message('type specialization')
20162021
export_macos_v3_report_content(macos_v3_compiler_error_fallback, 'v3', compiler_error, '',
2017-
[])
2022+
map[string]string{})
20182023
report := take_macos_v3_report_content() or {
20192024
assert false, 'the forwarded content report must be returned'
20202025
return

doc/docs.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -117,10 +117,10 @@ source excerpt is included it is always a **bounded strict subset** of the faili
117117
file — a window around the failure, or a head+tail window — and the whole file is
118118
never uploaded.
119119
An internal V3 compiler error on a short program (and any directory build such as
120-
`v .`) submits metadata only. If a single-file input changes while V3 is compiling
121-
it, the fallback also submits metadata only rather than source V3 did not parse. A
122-
generated-C error, however, maps to a specific V file, so it can still upload a
123-
strict-subset excerpt of that file plus a few lines of context around the failing
120+
`v .`) submits metadata only. If the input selected for a source excerpt changes
121+
after V3 parses it, that report also submits metadata only rather than source V3
122+
did not parse. An unchanged input mapped from a generated-C error can still upload
123+
a strict-subset excerpt of that file plus a few lines of context around the failing
124124
line, even when the file is short. Inline-assembly
125125
fallbacks are notice-only and do not submit a report; reporting is also skipped
126126
for test compilations and to the default endpoint in GitHub CI. A custom fallback

vlib/v/builder/c_error_report.v

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ module builder
22

33
import os
44
import strings
5+
import crypto.sha256
56
import v.pref
67
import v.gen.c as cgen
78
import v.util.version
@@ -298,11 +299,12 @@ pub fn export_external_v3_report_to_env(report ExternalCErrorBugReport) {
298299
// error. Internal compiler errors deliberately return no source here: their input must
299300
// be snapshotted before V3 starts and passed to bounded_v3_internal_fallback_source, so
300301
// a later editor/build-watcher rewrite cannot be uploaded as if V3 had parsed it.
301-
// `allowed_v_sources` is the set of source files that V3 actually parsed; generated-C
302-
// mappings outside that set are rejected before their contents are read. The returned
303-
// snippet is always a bounded strict subset (never a whole file); ('', '') means no
304-
// source is available, so the report stays metadata-only.
305-
pub fn bounded_v3_fallback_source(kind string, c_output string, c_file string, allowed_v_sources []string) (string, string) {
302+
// `allowed_v_sources` maps every source file V3 actually parsed to the SHA-256 digest
303+
// captured from those exact parser bytes. Generated-C mappings outside that set, or
304+
// files whose current contents no longer match the captured digest, contribute no
305+
// source. The returned snippet is always a bounded strict subset (never a whole file);
306+
// ('', '') means no source is available, so the report stays metadata-only.
307+
pub fn bounded_v3_fallback_source(kind string, c_output string, c_file string, allowed_v_sources map[string]string) (string, string) {
306308
if kind == external_v3_compiler_error_kind {
307309
return '', ''
308310
}
@@ -326,7 +328,7 @@ pub fn bounded_v3_internal_fallback_source(source_name string, source string) (s
326328
// source line it came from (via the #line directives in the trusted staged C) and returns
327329
// a bounded window of that V file. The generated C was staged by this process's own V3
328330
// run, while its mapped V path is trusted only when it matches a parsed compiler input.
329-
fn bounded_v_source_for_generated_c(c_output string, generated_c_file string, allowed_v_sources []string) (string, string) {
331+
fn bounded_v_source_for_generated_c(c_output string, generated_c_file string, allowed_v_sources map[string]string) (string, string) {
330332
c_source := os.read_file(generated_c_file) or { return '', '' }
331333
c_lines := c_source.split_into_lines()
332334
mut v_file := ''
@@ -340,10 +342,25 @@ fn bounded_v_source_for_generated_c(c_output string, generated_c_file string, al
340342
v_file = source_loc.file
341343
v_line = source_loc.line
342344
}
343-
if v_file == '' || !allowed_v_sources.any(same_path(it, v_file)) || !os.is_file(v_file) {
345+
if v_file == '' || !os.is_file(v_file) {
346+
return '', ''
347+
}
348+
mut parsed_digest := ''
349+
for parsed_file, digest in allowed_v_sources {
350+
if same_path(parsed_file, v_file) {
351+
parsed_digest = digest
352+
break
353+
}
354+
}
355+
if parsed_digest == '' {
344356
return '', ''
345357
}
346358
mapped_source := os.read_file(v_file) or { return '', '' }
359+
if sha256.hexhash(mapped_source) != parsed_digest {
360+
// An editor/build watcher rewrote this input after V3 parsed it. Keep the
361+
// report metadata-only rather than uploading bytes unrelated to the failure.
362+
return os.base(v_file), ''
363+
}
347364
mapped_lines := mapped_source.split_into_lines()
348365
chunk := selected_v_source(v_file, mapped_lines, v_line)
349366
mut v_source := bounded_v_source(chunk.text, c_error_bug_report_max_v_source_bytes, chunk.focus)

vlib/v/builder/c_error_report_test.v

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
module builder
22

33
import os
4+
import crypto.sha256
45
import v.pref
56

67
fn restore_env_var(name string, old_value ?string) {
@@ -329,7 +330,7 @@ fn test_external_v3_report_env_round_trip() {
329330
assert v_source.contains(c_error_v_source_truncation_notice)
330331
// The path-based extractor must never reopen an internal-error input after V3 fails.
331332
late_file, late_source := bounded_v3_fallback_source(external_v3_compiler_error_kind,
332-
'error: v3 failed', c_file, [])
333+
'error: v3 failed', c_file, map[string]string{})
333334
assert late_file == ''
334335
assert late_source == ''
335336
// ...then forwards only that content; export reads no path and deletes no directory.
@@ -629,7 +630,9 @@ fn test_bounded_v3_fallback_source_maps_generated_c_error() {
629630
os.write_file(generated_c, '#line 100 "${v_path}"\nint a = 1;\nint b = missing;\n')!
630631
c_output := '${generated_c}:3:9: error: use of undeclared identifier missing'
631632
// kind '' selects the generated-C mapping path.
632-
v_file, v_source := bounded_v3_fallback_source('', c_output, generated_c, [v_path])
633+
v_file, v_source := bounded_v3_fallback_source('', c_output, generated_c, {
634+
v_path: sha256.hexhash(whole)
635+
})
633636
assert v_file == 'source.v'
634637
assert v_source != ''
635638
// A bounded strict subset centered on the mapped V line (~100), never the whole file.
@@ -654,11 +657,14 @@ fn test_bounded_v3_fallback_source_rejects_nonblank_whole_file_generated_c() {
654657
lines << 'fn f${i}() { println(${i}) }'
655658
}
656659
// 80 substantive lines plus a whitespace-only final line (81 lines total).
657-
os.write_file(v_path, lines.join('\n') + '\n ')!
660+
whole := lines.join('\n') + '\n '
661+
os.write_file(v_path, whole)!
658662
generated_c := os.join_path(dir, 'program.tmp.c')
659663
os.write_file(generated_c, '#line 40 "${v_path}"\nint b = missing;\n')!
660664
c_output := '${generated_c}:2:9: error: use of undeclared identifier missing'
661-
v_file, v_source := bounded_v3_fallback_source('', c_output, generated_c, [v_path])
665+
v_file, v_source := bounded_v3_fallback_source('', c_output, generated_c, {
666+
v_path: sha256.hexhash(whole)
667+
})
662668
assert v_file == 'source.v'
663669
// The mapped window exposes every nonblank line, so no source is uploaded.
664670
assert v_source == '', v_source
@@ -681,13 +687,40 @@ fn test_bounded_v3_fallback_source_rejects_unparsed_mapped_file() {
681687
generated_c := os.join_path(dir, 'program.tmp.c')
682688
os.write_file(generated_c, '#line 1 "${unrelated_path}"\nint exposed = missing;\n')!
683689
c_output := '${generated_c}:2:15: error: use of undeclared identifier missing'
684-
v_file, v_source := bounded_v3_fallback_source('', c_output, generated_c, [
685-
parsed_path,
686-
])
690+
v_file, v_source := bounded_v3_fallback_source('', c_output, generated_c, {
691+
parsed_path: sha256.hexhash(os.read_file(parsed_path)!)
692+
})
687693
assert v_file == ''
688694
assert v_source == ''
689695
}
690696

697+
fn test_bounded_v3_fallback_source_rejects_changed_parsed_source() {
698+
dir := os.join_path(os.vtmp_dir(), 'v3_gen_c_changed_${os.getpid()}')
699+
os.rmdir_all(dir) or {}
700+
os.mkdir_all(dir) or { panic(err) }
701+
defer {
702+
os.rmdir_all(dir) or {}
703+
}
704+
v_path := os.join_path(dir, 'source.v')
705+
mut original_lines := []string{}
706+
for i in 0 .. 200 {
707+
original_lines << 'fn original_${i}() { println(${i}) }'
708+
}
709+
original := original_lines.join('\n')
710+
os.write_file(v_path, original)!
711+
parsed_digest := sha256.hexhash(original)
712+
generated_c := os.join_path(dir, 'program.tmp.c')
713+
os.write_file(generated_c, '#line 100 "${v_path}"\nint b = missing;\n')!
714+
// Simulate an editor/build watcher replacing the mapped input after V3 parsed it.
715+
os.write_file(v_path, original.replace('original_', 'new_private_'))!
716+
c_output := '${generated_c}:2:9: error: use of undeclared identifier missing'
717+
v_file, v_source := bounded_v3_fallback_source('', c_output, generated_c, {
718+
v_path: parsed_digest
719+
})
720+
assert v_file == 'source.v'
721+
assert v_source == ''
722+
}
723+
691724
fn test_generated_c_reset_line_is_not_reported_as_v_source() {
692725
c_lines := [
693726
'#line 40 "/tmp/program.tmp.c"',

vlib/v/help/build/build-c.txt

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,11 @@ see also `v help build`.
2222
be fixed: the V version, target and build options, plus a bounded snippet
2323
of the failing V source when one is available (always a strict subset,
2424
never the whole file). An internal V3 error on a short program or a
25-
directory build such as `v .` submits metadata only. A single-file input
26-
changed while V3 is compiling also submits metadata only, rather than source
27-
V3 did not parse. A generated-C error can still include a strict-subset
28-
excerpt. Inline-assembly fallbacks
25+
directory build such as `v .` submits metadata only. If the input selected
26+
for a source excerpt changes after V3 parses it, that report also submits
27+
metadata only, rather than source V3 did not parse. An unchanged input mapped
28+
from a generated-C error can still include a strict-subset excerpt.
29+
Inline-assembly fallbacks
2930
are notice-only; test compilations and GitHub CI also submit no report to
3031
the default endpoint. An explicitly configured custom bug report URL remains
3132
active in CI. See `v help build` for the full description of the default.

vlib/v/help/build/build.txt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,10 @@ The default compiler:
2424
https://bugs.vlang.io, plus a bounded snippet of the failing V source when one
2525
is available. Any snippet is a strict subset and the whole file is never
2626
uploaded. An internal V3 compiler error on a short program or a directory build
27-
such as `v .` submits metadata only. A single-file input changed while V3 is
28-
compiling also submits metadata only, rather than source V3 did not parse. A
29-
generated-C error maps to a V file and can still upload a strict-subset excerpt
30-
of it, even for a short file.
27+
such as `v .` submits metadata only. If the input selected for a source excerpt
28+
changes after V3 parses it, that report also submits metadata only, rather than
29+
source V3 did not parse. An unchanged input mapped from a generated-C error can
30+
still upload a strict-subset excerpt of it, even for a short file.
3131
Inline-assembly fallbacks are notice-only; test compilations and GitHub CI also
3232
submit no report to the default endpoint. A custom fallback URL set with
3333
V_C_ERROR_BUG_REPORT_URL remains active in CI. The -bug-report-url option

vlib/v3/driver/driver.v

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import runtime
55
import strconv
66
import strings
77
import time
8+
import crypto.sha256
89
import v3.ansi
910
import v3.bench
1011
import v3.cmdexec
@@ -52,6 +53,7 @@ const macos_v3_c_error_compiler_file = 'compiler'
5253
const macos_v3_c_error_output_file = 'output'
5354
const macos_v3_c_error_source_name_file = 'source_name'
5455
const macos_v3_c_error_v_sources_file = 'v_sources'
56+
const macos_v3_c_error_v_source_digests_file = 'v_source_digests'
5557

5658
fn configure_selfhost_parallelism(building_v bool) {
5759
if !building_v || os.getenv('VJOBS') != '' || os.getenv('V3_NO_SELFHOST_JOB_OVERCOMMIT') != '' {
@@ -6218,7 +6220,7 @@ fn v3_impure_v_diagnostics(a &flat.FlatAst) []parser.Diagnostic {
62186220
return diagnostics
62196221
}
62206222

6221-
fn request_macos_v3_c_error_fallback(fallback_file string, report_dir string, ccompiler string, c_output string, c_source string, v_sources []string) bool {
6223+
fn request_macos_v3_c_error_fallback(fallback_file string, report_dir string, ccompiler string, c_output string, c_source string, v_sources map[string]string) bool {
62226224
if fallback_file == '' || report_dir == '' || !os.is_file(c_source) {
62236225
return false
62246226
}
@@ -6242,7 +6244,25 @@ fn request_macos_v3_c_error_fallback(fallback_file string, report_dir string, cc
62426244
os.rmdir_all(report_dir) or {}
62436245
return false
62446246
}
6245-
os.write_file(os.join_path(report_dir, macos_v3_c_error_v_sources_file), v_sources.join('\x00')) or {
6247+
mut v_source_paths := v_sources.keys()
6248+
v_source_paths.sort()
6249+
mut v_source_paths_text := strings.new_builder(v_source_paths.len * 64)
6250+
mut v_source_digests_text := strings.new_builder(v_source_paths.len * (sha256.size * 2 + 1))
6251+
for i, path in v_source_paths {
6252+
if i > 0 {
6253+
v_source_paths_text.write_u8(0)
6254+
v_source_digests_text.write_u8(0)
6255+
}
6256+
v_source_paths_text.write_string(path)
6257+
v_source_digests_text.write_string(v_sources[path])
6258+
}
6259+
os.write_file(os.join_path(report_dir, macos_v3_c_error_v_sources_file),
6260+
v_source_paths_text.str()) or {
6261+
os.rmdir_all(report_dir) or {}
6262+
return false
6263+
}
6264+
os.write_file(os.join_path(report_dir, macos_v3_c_error_v_source_digests_file),
6265+
v_source_digests_text.str()) or {
62466266
os.rmdir_all(report_dir) or {}
62476267
return false
62486268
}
@@ -6253,7 +6273,7 @@ fn request_macos_v3_c_error_fallback(fallback_file string, report_dir string, cc
62536273
return true
62546274
}
62556275

6256-
fn request_macos_v3_c_error_fallback_from_message(fallback_file string, report_dir string, ccompiler string, message string, c_sources []string, v_sources []string) bool {
6276+
fn request_macos_v3_c_error_fallback_from_message(fallback_file string, report_dir string, ccompiler string, message string, c_sources []string, v_sources map[string]string) bool {
62576277
is_c_error := message.starts_with('failed to build C object ')
62586278
|| message.starts_with('failed to build cached module object ')
62596279
|| message.starts_with('failed to build cached program prefix:')
@@ -6270,16 +6290,28 @@ fn request_macos_v3_c_error_fallback_from_message(fallback_file string, report_d
62706290
return false
62716291
}
62726292

6273-
fn macos_v3_fallback_report_sources(a &flat.FlatAst) []string {
6274-
mut sources := map[string]bool{}
6293+
fn macos_v3_fallback_report_sources(a &flat.FlatAst) map[string]string {
6294+
mut sources := map[string]string{}
6295+
mut ambiguous := map[string]bool{}
62756296
for _, file in a.source_files {
6276-
if file.name.ends_with('.v') || file.name.ends_with('.vv') || file.name.ends_with('.vsh') {
6277-
sources[os.real_path(file.name)] = true
6297+
if (file.name.ends_with('.v') || file.name.ends_with('.vv')
6298+
|| file.name.ends_with('.vsh')) && file.has_source_sha256() {
6299+
path := os.real_path(file.name)
6300+
source_digest := file.source_sha256()
6301+
digest := source_digest[..].hex()
6302+
if old_digest := sources[path] {
6303+
if old_digest != digest {
6304+
ambiguous[path] = true
6305+
}
6306+
} else {
6307+
sources[path] = digest
6308+
}
62786309
}
62796310
}
6280-
mut paths := sources.keys()
6281-
paths.sort()
6282-
return paths
6311+
for path, _ in ambiguous {
6312+
sources.delete(path)
6313+
}
6314+
return sources
62836315
}
62846316

62856317
fn input_uses_minimal_literal_output_builtin(input_file string, prefs &pref.Preferences, is_test_command bool, is_checker_fixture bool) bool {

0 commit comments

Comments
 (0)