Skip to content

Commit 6767c91

Browse files
committed
v3: snapshot internal fallback report source
1 parent ae7bfea commit 6767c91

7 files changed

Lines changed: 114 additions & 45 deletions

File tree

cmd/v/macos_v3_dispatch.c.v

Lines changed: 55 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ module main
1212
// the way. The `macos_v3_` name is kept for continuity with the original macOS
1313
// rollout even though the behavior now also covers Linux.
1414
import os
15+
import crypto.sha256
1516
import v.pref
1617
import v.util
1718
import v.builder
@@ -44,6 +45,13 @@ const macos_v3_c_error_kind_file = 'kind'
4445
// the receiver's c_error_string parser stores a nonempty, groupable diagnostic).
4546
const macos_v3_compiler_error_message_base = 'error: the experimental V3 compiler hit an internal compiler error building this program'
4647

48+
struct MacosV3InputSnapshot {
49+
path string
50+
digest string
51+
v_file string
52+
v_source string
53+
}
54+
4755
fn macos_v3_compiler_error_message(stage string) string {
4856
stage_suffix := if stage == '' { '' } else { ' during ${stage}' }
4957
return '${macos_v3_compiler_error_message_base}${stage_suffix} (the stable V compiler built it successfully)'
@@ -217,13 +225,14 @@ fn launch_macos_v3_compiler(prefs &pref.Preferences, raw_args []string) ?MacosV3
217225
}
218226
replace_macos_v3_process_environment(environment)
219227
is_verbose := prefs.is_verbose
220-
// The input path is captured before V3 runs so the compatibility fallback can
221-
// stage it for a bug report if V3 fails with an internal compiler error.
222-
input_path := prefs.path
228+
// Capture and bound the source before V3 runs. If an editor or build watcher rewrites
229+
// the input while V3 is compiling, the fallback will detect the changed digest and
230+
// submit metadata only instead of reading/uploading bytes V3 never parsed.
231+
input_snapshot := macos_v3_compiler_error_input_snapshot(prefs.path)
223232
retry_args := os.args[1..].clone()
224-
at_exit(fn [caller_environment, fallback_file, c_error_dir, retry_args, is_verbose, input_path] () {
233+
at_exit(fn [caller_environment, fallback_file, c_error_dir, retry_args, is_verbose, input_snapshot] () {
225234
retry_macos_v3_with_old_compiler(caller_environment, fallback_file, c_error_dir,
226-
retry_args, is_verbose, input_path)
235+
retry_args, is_verbose, input_snapshot)
227236
}) or {
228237
eprintln('cannot register the V3 compatibility fallback: ${err}')
229238
exit(1)
@@ -246,7 +255,7 @@ fn replace_macos_v3_process_environment(environment map[string]string) {
246255
}
247256
}
248257

249-
fn retry_macos_v3_with_old_compiler(caller_environment map[string]string, fallback_file string, c_error_dir string, retry_args []string, is_verbose bool, input_path string) {
258+
fn retry_macos_v3_with_old_compiler(caller_environment map[string]string, fallback_file string, c_error_dir string, retry_args []string, is_verbose bool, input_snapshot MacosV3InputSnapshot) {
250259
fallback_payload := os.read_file(fallback_file) or { return }
251260
fallback_reason, fallback_stage := macos_v3_fallback_reason_and_stage(fallback_payload)
252261
os.rm(fallback_file) or {}
@@ -289,14 +298,13 @@ fn retry_macos_v3_with_old_compiler(caller_environment map[string]string, fallba
289298
}
290299
} else if fallback_reason == macos_v3_compiler_error_fallback {
291300
// V3 hit an internal compiler error (parser/checker/codegen) that V1 may still
292-
// handle. Bound the input V source HERE (trusted) and forward it to the retry as
293-
// content, so once the retry's build succeeds it prints the fallback notice and
294-
// (for a single-file build) files a bug with the source. A directory build (`v .`)
295-
// or non-V input yields no source, so the report stays metadata-only but the
296-
// fallback is never silent.
297-
export_macos_v3_report_content(macos_v3_compiler_error_fallback, 'v3',
298-
macos_v3_compiler_error_message(fallback_stage),
299-
macos_v3_compiler_error_input_source(input_path), [])
301+
// handle. Forward the pre-V3 source snapshot only if the input still has the same
302+
// digest; otherwise send metadata only, never bytes that V3 did not parse. Once the
303+
// retry succeeds it prints the fallback notice and files the report. A directory
304+
// build (`v .`) or non-V input also remains metadata-only, but never silent.
305+
v_file, v_source := input_snapshot.current_report_source()
306+
export_macos_v3_bounded_report_content(macos_v3_compiler_error_fallback, 'v3',
307+
macos_v3_compiler_error_message(fallback_stage), v_file, v_source)
300308
os.rmdir_all(c_error_dir) or {}
301309
if should_report {
302310
eprintln('V3 compilation failed; retrying with `-old-compiler`.')
@@ -343,6 +351,10 @@ fn take_macos_v3_report_content() ?MacosV3CErrorReport {
343351
// retry through the environment.
344352
fn export_macos_v3_report_content(kind string, ccompiler string, c_output string, c_file string, v_sources []string) {
345353
v_file, v_source := builder.bounded_v3_fallback_source(kind, c_output, c_file, v_sources)
354+
export_macos_v3_bounded_report_content(kind, ccompiler, c_output, v_file, v_source)
355+
}
356+
357+
fn export_macos_v3_bounded_report_content(kind string, ccompiler string, c_output string, v_file string, v_source string) {
346358
builder.export_external_v3_report_to_env(builder.ExternalCErrorBugReport{
347359
kind: kind
348360
ccompiler: ccompiler
@@ -354,6 +366,35 @@ fn export_macos_v3_report_content(kind string, ccompiler string, c_output string
354366
})
355367
}
356368

369+
fn macos_v3_compiler_error_input_snapshot(input_path string) MacosV3InputSnapshot {
370+
candidate := macos_v3_compiler_error_input_source(input_path)
371+
if candidate == '' {
372+
return MacosV3InputSnapshot{}
373+
}
374+
// Preserve the caller's symlink semantics while making the path independent of any
375+
// working-directory changes inside V3.
376+
v_path := os.abs_path(candidate)
377+
source := os.read_file(v_path) or { return MacosV3InputSnapshot{} }
378+
v_file, v_source := builder.bounded_v3_internal_fallback_source(v_path, source)
379+
return MacosV3InputSnapshot{
380+
path: v_path
381+
digest: sha256.hexhash(source)
382+
v_file: v_file
383+
v_source: v_source
384+
}
385+
}
386+
387+
fn (snapshot MacosV3InputSnapshot) current_report_source() (string, string) {
388+
if snapshot.path == '' || snapshot.digest == '' {
389+
return '', ''
390+
}
391+
current := os.read_file(snapshot.path) or { return '', '' }
392+
if sha256.hexhash(current) != snapshot.digest {
393+
return '', ''
394+
}
395+
return snapshot.v_file, snapshot.v_source
396+
}
397+
357398
// macos_v3_compiler_error_input_source returns `input_path` when it is a single V source
358399
// file whose bounded contents can be uploaded, or '' for a directory / non-V input (which
359400
// keeps the internal-error report metadata-only).

cmd/v/macos_v3_test.v

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1665,21 +1665,26 @@ fn test_macos_v3_compiler_error_content_extraction() {
16651665
whole := lines.join('\n')
16661666
os.write_file(source, whole)!
16671667
compiler_error := macos_v3_compiler_error_message('source parsing')
1668-
v_file, v_source := builder.bounded_v3_fallback_source(macos_v3_compiler_error_fallback,
1669-
compiler_error, macos_v3_compiler_error_input_source(source), [])
1668+
snapshot := macos_v3_compiler_error_input_snapshot(source)
1669+
v_file, v_source := snapshot.current_report_source()
16701670
assert v_file == 'prog.v'
16711671
assert v_source != ''
16721672
assert compiler_error.contains('during source parsing')
16731673
// A bounded strict subset — never the whole file.
16741674
assert v_source.len < whole.len
1675+
// Rewriting the file after the pre-V3 snapshot suppresses source completely: the
1676+
// fallback must not upload bytes that V3 never parsed.
1677+
os.write_file(source, whole + '\nfn changed_after_snapshot() {}')!
1678+
changed_file, changed_source := snapshot.current_report_source()
1679+
assert changed_file == ''
1680+
assert changed_source == ''
16751681
// A directory build, a non-V file, or a missing input yields no source, so the
16761682
// report stays metadata-only.
16771683
note := os.join_path(root, 'note.txt')
16781684
os.write_file(note, 'not v source')!
16791685
for empty in [root, note, os.join_path(root, 'missing.v'), ''] {
1680-
resolved := macos_v3_compiler_error_input_source(empty)
1681-
ef, es := builder.bounded_v3_fallback_source(macos_v3_compiler_error_fallback,
1682-
compiler_error, resolved, [])
1686+
empty_snapshot := macos_v3_compiler_error_input_snapshot(empty)
1687+
ef, es := empty_snapshot.current_report_source()
16831688
assert ef == '', empty
16841689
assert es == '', empty
16851690
}

doc/docs.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,9 +117,11 @@ 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. A generated-C error, however, maps to a specific V
121-
file, so it can still upload a strict-subset excerpt of that file plus a few lines
122-
of context around the failing line, even when the file is short. Inline-assembly
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
124+
line, even when the file is short. Inline-assembly
123125
fallbacks are notice-only and do not submit a report; reporting is also skipped
124126
for test compilations and to the default endpoint in GitHub CI. A custom fallback
125127
endpoint set with `V_C_ERROR_BUG_REPORT_URL` remains active in CI. The

vlib/v/builder/c_error_report.v

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -294,23 +294,34 @@ pub fn export_external_v3_report_to_env(report ExternalCErrorBugReport) {
294294
// bounded_v3_fallback_source extracts the bounded V source snippet to upload for a V3->V1
295295
// fallback, reading ONLY files the caller already trusts — it must be invoked by the
296296
// process that staged the report, never by one that merely inherited a report path from
297-
// the environment. `c_file` is the user's V source for a V3 internal error, or the staged
298-
// generated C for a generated-C compilation error. `allowed_v_sources` is the set of
299-
// source files that V3 actually parsed; generated-C mappings outside that set are
300-
// rejected before their contents are read. The returned snippet is always a bounded
301-
// strict subset (never a whole file); ('', '') means no source is available (e.g. a
302-
// directory build), so the report stays metadata-only.
297+
// the environment. `c_file` is the staged generated C for a generated-C compilation
298+
// error. Internal compiler errors deliberately return no source here: their input must
299+
// be snapshotted before V3 starts and passed to bounded_v3_internal_fallback_source, so
300+
// 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.
303305
pub fn bounded_v3_fallback_source(kind string, c_output string, c_file string, allowed_v_sources []string) (string, string) {
304-
if c_file == '' || !os.is_file(c_file) {
306+
if kind == external_v3_compiler_error_kind {
305307
return '', ''
306308
}
307-
if kind == external_v3_compiler_error_kind {
308-
src := os.read_file(c_file) or { return '', '' }
309-
return os.base(c_file), v3_report_v_source(src)
309+
if c_file == '' || !os.is_file(c_file) {
310+
return '', ''
310311
}
311312
return bounded_v_source_for_generated_c(c_output, c_file, allowed_v_sources)
312313
}
313314

315+
// bounded_v3_internal_fallback_source bounds source CONTENT captured before V3 starts.
316+
// It never opens `source_name`; the dispatcher separately verifies that the input still
317+
// has the captured digest before forwarding this snippet to the stable retry.
318+
pub fn bounded_v3_internal_fallback_source(source_name string, source string) (string, string) {
319+
if source_name == '' {
320+
return '', ''
321+
}
322+
return os.base(source_name), v3_report_v_source(source)
323+
}
324+
314325
// bounded_v_source_for_generated_c maps a generated-C compilation error back to the V
315326
// source line it came from (via the #line directives in the trusted staged C) and returns
316327
// a bounded window of that V file. The generated C was staged by this process's own V3
@@ -376,15 +387,17 @@ pub fn take_external_v3_report_from_env() ?ExternalCErrorBugReport {
376387
return report
377388
}
378389

379-
// submit_external_v3_compiler_error_bug_report reports a V3 internal compiler error
380-
// after the stable compiler has confirmed the program is buildable. `v_file` is the path
381-
// to the user's input V source, which this reads and bounds into the uploaded snippet;
382-
// `v3_output` is a short description of the failure. This file-reading form is used only
383-
// on the trusted in-process build path, where `v_file` is not caller-forgeable.
390+
// submit_external_v3_compiler_error_bug_report reports metadata for a V3 internal
391+
// compiler error after the stable compiler has confirmed the program is buildable.
392+
// Source may only be supplied through the pre-V3 snapshot/content path, so this legacy
393+
// path form deliberately does not reopen `v_file` after the failure.
384394
pub fn submit_external_v3_compiler_error_bug_report(prefs &pref.Preferences, v3_stage string, v3_output string, v_file string, tag string) {
385-
v_source := if v_file == '' { '' } else { v3_report_v_source(os.read_file(v_file) or { '' }) }
386395
mut b := new_builder(prefs)
387-
b.submit_v3_compiler_error_bug_report(v3_stage, v3_output, v_file, v_source, tag)
396+
b.submit_v3_compiler_error_bug_report(v3_stage, v3_output, if v_file == '' {
397+
''
398+
} else {
399+
os.base(v_file)
400+
}, '', tag)
388401
}
389402

390403
// submit_inline_v3_compiler_error_bug_report reports a V3 internal compiler error whose

vlib/v/builder/c_error_report_test.v

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -323,11 +323,15 @@ fn test_external_v3_report_env_round_trip() {
323323
}
324324
os.write_file(c_file, lines.join('\n'))!
325325
// The owning process bounds the source into content...
326-
v_file, v_source := bounded_v3_fallback_source(external_v3_compiler_error_kind,
327-
'error: v3 failed', c_file, [])
326+
v_file, v_source := bounded_v3_internal_fallback_source(c_file, lines.join('\n'))
328327
assert v_file == 'main.v'
329328
assert v_source != ''
330329
assert v_source.contains(c_error_v_source_truncation_notice)
330+
// The path-based extractor must never reopen an internal-error input after V3 fails.
331+
late_file, late_source := bounded_v3_fallback_source(external_v3_compiler_error_kind,
332+
'error: v3 failed', c_file, [])
333+
assert late_file == ''
334+
assert late_source == ''
331335
// ...then forwards only that content; export reads no path and deletes no directory.
332336
export_external_v3_report_to_env(ExternalCErrorBugReport{
333337
kind: external_v3_compiler_error_kind

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,10 @@ 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, while a generated-C
26-
error can still include a strict-subset excerpt. Inline-assembly fallbacks
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
2729
are notice-only; test compilations and GitHub CI also submit no report to
2830
the default endpoint. An explicitly configured custom bug report URL remains
2931
active in CI. See `v help build` for the full description of the default.

vlib/v/help/build/build.txt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +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, while a generated-C error maps to a V file
28-
and can still upload a strict-subset excerpt of it, even for a short file.
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.
2931
Inline-assembly fallbacks are notice-only; test compilations and GitHub CI also
3032
submit no report to the default endpoint. A custom fallback URL set with
3133
V_C_ERROR_BUG_REPORT_URL remains active in CI. The -bug-report-url option

0 commit comments

Comments
 (0)