fastc: declare thread handle types at .wait() sites too (#31124)

The thread handle typedef and its waiter were only registered by `spawn`.
A `[]thread T` that is waited on in code whose only `spawn` is compiled
out by `$if` referenced both without defining them. The driver does this
with its prestarted FastC workers, which only macOS spawns, so FastC
builds of cmd/v failed on Linux with
`'__vf_thread_kF_5f_5fFastcPrestartedCUnits' undeclared`.

`.wait()` lowering now registers the typedef and waiter when the file
has not done so yet.
This commit is contained in:
Alexander Medvednikov authored and GitHub committed 2026-10-25 18:29:11 +03:00
1 parent c984dc6e33
commit 1be458cf31
3 files changed
+73 -7

No files matched your search

+2 -2
View File
@@ -477,7 +477,7 @@ fn (g &Parser) render_method_call_expression(tokens []FastcExpressionToken, rend
}
mut w := unsafe { &Parser(g) }
fastc_register_composite_type(result_type, mut w.composite_types)
wait_helper := g.fastc_unclaimed_generated_name(fastc_thread_wait_name(flat.source_type_name(element)))
wait_helper := g.fastc_thread_wait_helper(flat.source_type_name(element), value_type)
recv_src := if flat.source_type_name(receiver_type).ends_with('*') {
'*(${receiver.source})'
} else {
@@ -517,7 +517,7 @@ fn (g &Parser) render_method_call_expression(tokens []FastcExpressionToken, rend
wait_end := fastc_matching_rpar(tokens, i + 1) or { continue }
receiver := g.render_method_receiver_expression(receiver_tokens) or { continue }
value_type := (g.fastc_thread_value_type(receiver_type) or { u32(0) })
wait_call := '${g.fastc_unclaimed_generated_name(fastc_thread_wait_name(flat.source_type_name(receiver_type)))}(${receiver.source})'
wait_call := '${g.fastc_thread_wait_helper(flat.source_type_name(receiver_type).trim_right('*'), value_type)}(${receiver.source})'
if receiver_start == 0 && wait_end == tokens.len - 1 {
return FastcRenderedExpression{
source: wait_call
+26 -5
View File
@@ -97,6 +97,18 @@ fn fastc_thread_wait_name(thread_type string) string {
return '__vf_thread_wait_${thread_type.all_after(fastc_thread_type_prefix)}'
}
// fastc_thread_wait_helper returns the waiter of one thread handle type,
// registering its typedef and waiter when no `spawn` has done so yet.
fn (g &Parser) fastc_thread_wait_helper(thread_type string, value_type_source_id u32) string {
wait_name := g.fastc_unclaimed_generated_name(fastc_thread_wait_name(thread_type))
if thread_type in g.spawn_typedefs && wait_name in g.spawn_helpers {
return wait_name
}
// Rendering keeps an immutable parser; the helper maps are per-file output.
mut w := unsafe { &Parser(g) }
return w.register_thread_type_helpers(thread_type, value_type_source_id)
}
// read_spawn_expression parses `spawn callee(arguments)` eagerly at an
// expression start. The callee must be a collected plain or imported
// function; its signature supplies the packed-argument field types.
@@ -390,11 +402,10 @@ fn (g &Parser) generated_name_is_claimed(candidate string) bool {
return false
}
// register_spawn_helpers records the thread typedef, waiter, packed-argument
// struct, run wrapper, and creator for one spawn target. The result is the
// first packed field, so the per-type waiter reads it without knowing which
// call site produced the thread.
fn (mut g Parser) register_spawn_helpers(function_key string, thread_type string, value_type_source_id u32, parameter_types []u32, start_name string, target_c_name string) {
// register_thread_type_helpers records the typedef and waiter of one thread
// handle type, and returns the waiter name. `.wait()` calls register them too:
// a handle can be waited on in code whose only `spawn` is compiled out by `$if`.
fn (mut g Parser) register_thread_type_helpers(thread_type string, value_type_source_id u32) string {
value_type := flat.source_type_name(value_type_source_id)
g.type_memo.clear()
g.thread_value_types.set_id(thread_type, value_type_source_id)
@@ -430,6 +441,16 @@ fn (mut g Parser) register_spawn_helpers(function_key string, thread_type string
}
g.spawn_helpers[wait_name] = waiter
}
return wait_name
}
// register_spawn_helpers records the thread typedef, waiter, packed-argument
// struct, run wrapper, and creator for one spawn target. The result is the
// first packed field, so the per-type waiter reads it without knowing which
// call site produced the thread.
fn (mut g Parser) register_spawn_helpers(function_key string, thread_type string, value_type_source_id u32, parameter_types []u32, start_name string, target_c_name string) {
value_type := flat.source_type_name(value_type_source_id)
g.register_thread_type_helpers(thread_type, value_type_source_id)
if start_name in g.spawn_helpers {
return
}
@@ -55,3 +55,48 @@ fn main() {
assert run.exit_code == 0, run.output
assert run.output.trim_space() == 'thread arrays work', run.output
}
fn test_fastc_waits_on_thread_handles_whose_spawn_is_compiled_out() {
$if !macos && !linux {
return
}
vroot := os.real_path(os.join_path(@DIR, '../../../..'))
root := os.join_path(os.vtmp_dir(), 'fastc_thread_compiled_out_spawn_${os.getpid()}')
os.mkdir_all(root) or { panic(err) }
defer { os.rmdir_all(root) or {} }
// The driver waits on prestarted workers that only macOS spawns; the
// thread typedefs and waiters must exist without any reachable `spawn`.
os.write_file(os.join_path(root, 'main.v'), r'module main
struct Item { value int }
fn item(value int) Item { return Item{value: value} }
fn integer(value int) int { return value }
fn nothing() {}
fn main() {
mut items := []thread Item{}
mut integers := []thread int{}
mut completed := []thread{}
$if fastc_thread_never ? {
items << spawn item(1)
integers << spawn integer(2)
completed << spawn nothing()
}
if items.len > 0 {
assert items[0].wait().value == 1
}
values := integers.wait()
completed.wait()
println("waited ${items.len} ${values.len} ${completed.len}")
}
') or { panic(err) }
program := os.join_path(root, 'program')
build := cmdexec.run_in_merged_with_env(@VEXE, ['-b', 'fastc', '-nocache', '-gc', 'none', '-o',
program, root], vroot, {
'VEXE': @VEXE
'VFLAGS': ''
'V_MACOS_V3_NO_FALLBACK': '1'
})
assert build.exit_code == 0, build.output
run := cmdexec.run(program, [])
assert run.exit_code == 0, run.output
assert run.output.trim_space() == 'waited 0 0 0', run.output
}