fastc: fix production PGO self-build training

This commit is contained in:
Alexander Medvednikov committed 2026-10-28 00:06:51 +03:00
1 parent 3ebaeb81d6
commit 748347064f
7 files changed
+163 -16

No files matched your search

+5
View File
@@ -235,6 +235,11 @@ v -b fastc -nocache -o v_fastc1 cmd/v
Each generation retains the full command-line entry and can compile programs with either
the C or FastC backend. `-nocache` forces each generation to rebuild its generated C units.
Clang production self-builds also exercise this full-compiler FastC path during PGO training
when FastC is included. Compiler sources must stay within the FastC self-hosting subset for
that training to complete.
This path preserves enum shorthand types in parallel assignments and imported call arguments,
and lowers flag enum `.has(flag)` calls on variables and imported function results.
On macOS ARM64, standard full-compiler builds compile the owned native support files together
so TinyCC processes shared SDK declarations once. Debug builds, custom preprocessing flags,
and other native source sets retain separate compilation. FastC does not read their C headers.
@@ -50,6 +50,24 @@ fn run_with_v_environment(program string, args []string, flags string, vexe stri
return cmdexec.run(program, args)
}
fn test_full_compiler_fastc_pgo_training_generates_c() {
$if !macos && !linux {
return
}
root := os.join_path(os.vtmp_dir(), 'v_fastc_pgo_training_${os.getpid()}')
os.mkdir_all(root) or { panic(err) }
defer {
os.rmdir_all(root) or {}
}
output := os.join_path(root, 'cmd_v_fastc_training.c')
// Match the FastC profiling workload in vself without building an instrumented compiler.
training := run_with_v_environment(@VEXE, ['-nocache', '-gc', 'none', '-prealloc',
'-compile-backend', 'fastc', '-building-v', '-b', 'fastc', '-no-memory-limit', '-o', output,
os.join_path(fastc_backend_vroot, 'cmd', 'v')], '', @VEXE)
assert training.exit_code == 0, training.output
assert os.file_size(output) > 0
}
fn test_direct_fastc_compiler_entry_selects_selfhost_driver() {
$if !macos && !linux {
return
+4 -2
View File
@@ -3668,7 +3668,9 @@ fn (g &Parser) streaming_call_argument_type(tokens []FastcExpressionToken) u32 {
}
arg_index := fastc_call_argument_index(tokens, open)
name := tokens[open - 1].lit
if open >= 2 && tokens[open - 2].tok == .dot {
if open >= 2 && tokens[open - 2].tok == .dot
&& !(open >= 3 && tokens[open - 3].tok == .name && tokens[open - 3].lit in g.imports
&& tokens[open - 3].lit !in g.locals && (open < 4 || tokens[open - 4].tok != .dot)) {
// `recv.method(…)`: resolve on the receiver's type; parameter_types[0] is the receiver.
receiver_start := fastc_method_receiver_start(tokens, open - 2)
receiver_tokens := tokens[receiver_start..open - 2]
@@ -3682,7 +3684,7 @@ fn (g &Parser) streaming_call_argument_type(tokens []FastcExpressionToken) u32 {
u32(0)
}
}
function_key := g.unqualified_function_key(name)
function_key := g.function_key_for_call(tokens, open - 1)
signature := g.functions[function_key] or { return u32(0) }
return if arg_index < signature.parameter_types.ids.len {
signature.parameter_types.ids[arg_index]
@@ -0,0 +1,89 @@
module fastc
import os
import v.cmdexec
fn test_fastc_selfhost_enum_context_and_imported_flag_receiver() {
$if !macos && !linux {
return
}
root := os.join_path(os.vtmp_dir(), 'fastc_flag_receiver_${os.getpid()}')
os.mkdir_all(os.join_path(root, 'features')) or { panic(err) }
defer { os.rmdir_all(root) or {} }
os.write_file(os.join_path(root, 'v.mod'), 'Module { name: "flag_receiver" }') or { panic(err) }
os.write_file(os.join_path(root, 'features', 'features.v'), r'module features
@[flag]
pub enum Flags {
read
write
}
pub struct Counter {
pub mut:
value int
}
pub fn select(mut calls Counter) Flags {
calls.value++
return .read
}
pub fn accepts(flag Flags) bool {
return flag == .read
}
pub fn has_read(flag Flags) ?bool {
return flag.has(.read)
}
pub fn assign(mut flag Flags) {
mut count := 0
count, flag = 1, .read
assert count == 1
}
') or { panic(err) }
os.write_file(os.join_path(root, 'main.v'), r'module main
import features
fn main() {
mut calls := features.Counter{}
required := features.Flags.read
excluded := features.Flags.write
assert features.select(mut calls).has(required)
assert features.select(mut calls).has(.read)
assert !features.select(mut calls).has(excluded)
assert calls.value == 3
mut flag := features.Flags.write
assert flag.has(excluded)
mut count := 0
count, flag = 7, .read
assert count == 7
assert flag == .read
calls.value, flag = 8, .write
assert calls.value == 8
assert flag == .write
flag, calls.value = .read, 9
assert calls.value == 9
assert flag == .read
flag = .write
features.assign(mut flag)
assert flag == .read
assert features.accepts(if count == 7 { .read } else { .write })
assert !features.accepts(if count == 8 { .read } else { .write })
assert features.has_read(required) or { false }
assert !(features.has_read(excluded) or { true })
println("flag receivers work")
}
') or { panic(err) }
for backend in ['c', 'fastc'] {
executable := os.join_path(root, 'program_${backend}')
build := cmdexec.run(@VEXE, ['-b', backend, '-building-v', '-nocache', '-gc', 'none', '-o',
executable, root])
assert build.exit_code == 0, '${backend}: ${build.output}'
run := cmdexec.run(executable, [])
assert run.exit_code == 0, '${backend}: ${run.output}'
assert run.output.trim_space() == 'flag receivers work', '${backend}: ${run.output}'
}
}
+5 -2
View File
@@ -562,7 +562,7 @@ fn (g &Parser) render_c_struct_sizeof(tokens []FastcExpressionToken) ?FastcRende
}
fn (g &Parser) render_flag_method_expression(tokens []FastcExpressionToken, rendered_expression string) ?FastcRenderedExpression {
if tokens.len < 7 {
if tokens.len < 6 {
return none
}
mut rendered := rendered_expression
@@ -613,7 +613,10 @@ fn (g &Parser) render_flag_method_expression(tokens []FastcExpressionToken, rend
}
method := tokens[i].lit
raw_receiver_source := fastc_take_string(mut receiver)
receiver_source := g.render_member_receiver(receiver_tokens) or { raw_receiver_source }
receiver_source := g.render_member_receiver(receiver_tokens) or {
// Imported calls must resolve their C name and render their arguments too.
g.render_call_argument_expression(receiver_tokens, receiver_type) or { raw_receiver_source }
}
raw_argument_source := fastc_take_string(mut raw_argument)
c_argument_source := fastc_take_string(mut c_argument)
mut needle := '${raw_receiver_source}.${method}(${c_argument_source})'
+4
View File
@@ -266,6 +266,10 @@ fn (g &Parser) render_method_call_expression(tokens []FastcExpressionToken, rend
if !flags_processed && tokens[i].lit in ['has', 'set', 'clear'] {
flags_processed = true
if flags := g.render_flag_method_expression(tokens, rendered) {
call_end := fastc_matching_rpar(tokens, i + 1) or { -1 }
if receiver_start == 0 && call_end == tokens.len - 1 {
return flags
}
rendered = flags.source
changed = true
}
+38 -12
View File
@@ -964,11 +964,20 @@ fn (mut g Parser) parse_match_statement() !bool {
mut had_member_smartcast := false
mut member_smartcast_active := false
common_numeric_type := fastc_match_common_numeric_variant(variant_types)
mut array_variant_count := 0
if variant_types.len > 1 {
// Keep this scan within the FastC self-hosting subset used by PGO training.
for variant_type in variant_types {
if flat.source_type_query(variant_type).has(.c_array_prefix) {
array_variant_count++
}
}
}
// Several struct variants in one arm (`FnTypeDecl, AliasTypeDecl { node.type_pos }`)
// may only read fields common to all of them, which V lays out compatibly, so a
// plain-local subject narrows through the first variant — mirroring the member-chain
// smart-cast below.
struct_multi_variant := variant_types.len > 1 && common_numeric_type == 0 && !variant_types.any(flat.source_type_query(it).has(.c_array_prefix))
struct_multi_variant := variant_types.len > 1 && common_numeric_type == 0 && array_variant_count == 0
smartcast_active := is_boxed && !is_else && (variant_types.len == 1 || common_numeric_type != 0) && subject_local != ''
if smartcast_active {
variant_cname := if common_numeric_type != 0 {
@@ -1027,7 +1036,7 @@ fn (mut g Parser) parse_match_statement() !bool {
// layout for array variants) is correct.
member_smartcast_type := flat.source_type_id(if variant_types.len == 1 {
flat.source_type_name(variant_types[0])
} else if variant_types.len > 1 && variant_types.all(flat.source_type_query(it).has(.c_array_prefix)) {
} else if variant_types.len > 1 && array_variant_count == variant_types.len {
'array'
} else if variant_types.len > 1 && common_numeric_type == 0 {
flat.source_type_name(variant_types[0])
@@ -2162,10 +2171,22 @@ fn (mut g Parser) parse_parallel_assignment(initial_names []string, initial_mut
if g.selfhost && names.len > 1 && g.parallel_rhs_is_option_tuple_or() {
return g.parse_parallel_option_tuple(names, mutability, is_declaration)
}
assignment_targets := if is_declaration {
[]FastcRenderedExpression{}
} else {
g.validate_parallel_assignment_targets(names)!
}
g.last_multi_return_types = flat.TypeTextList{ ids: ([]u32{}).clone() }
mut values := []string{}
mut value_types := []u32{}
previous_expected_type := g.expected_expression_type
for {
// Each assignment value takes its context from the corresponding target.
g.expected_expression_type = if values.len < assignment_targets.len {
assignment_targets[values.len].typ
} else {
u32(0)
}
item := g.read_expression([token.Token.comma, token.Token.semicolon, token.Token.rcbr])!
if item == '' {
return g.unsupported('empty parallel assignment')
@@ -2177,6 +2198,7 @@ fn (mut g Parser) parse_parallel_assignment(initial_names []string, initial_mut
}
g.next()
}
g.expected_expression_type = previous_expected_type
value := values[0]
if value == '' {
return g.unsupported('empty parallel assignment')
@@ -2186,11 +2208,6 @@ fn (mut g Parser) parse_parallel_assignment(initial_names []string, initial_mut
if values.len != names.len {
return g.unsupported('parallel assignment with ${names.len} targets and ${values.len} values')
}
assignment_targets := if is_declaration {
[]FastcRenderedExpression{}
} else {
g.validate_parallel_assignment_targets(names)!
}
mut temporaries := []string{cap: values.len}
for i, item in values {
temporary := g.temporary_name('parallel')
@@ -2228,11 +2245,6 @@ fn (mut g Parser) parse_parallel_assignment(initial_names []string, initial_mut
if component_types.len == 0 {
component_types = g.last_multi_return_types.ids.clone()
}
assignment_targets := if is_declaration {
[]FastcRenderedExpression{}
} else {
g.validate_parallel_assignment_targets(names)!
}
temporary := g.temporary_name('multi_return')
g.write_line('MultiReturn ${temporary} = (${value});')
for i, name in names {
@@ -2286,7 +2298,13 @@ fn (mut g Parser) finish_parallel_member_assignment(names []string, member_targe
g.last_multi_return_types = flat.TypeTextList{ ids: ([]u32{}).clone() }
mut values := []string{}
mut value_types := []u32{}
previous_expected_type := g.expected_expression_type
for {
g.expected_expression_type = if values.len < targets.len {
targets[values.len].typ
} else {
u32(0)
}
value := g.read_expression([token.Token.comma, token.Token.semicolon, token.Token.rcbr])!
if value == '' {
return g.unsupported('empty parallel assignment')
@@ -2298,6 +2316,7 @@ fn (mut g Parser) finish_parallel_member_assignment(names []string, member_targe
}
g.next()
}
g.expected_expression_type = previous_expected_type
g.consume_statement_end()
if values.len > 1 {
if values.len != targets.len {
@@ -2574,7 +2593,13 @@ fn (mut g Parser) parse_parallel_expression_assignment(first_source string, firs
g.last_multi_return_types = flat.TypeTextList{ ids: ([]u32{}).clone() }
mut values := []string{}
mut value_types := []u32{}
previous_expected_type := g.expected_expression_type
for {
g.expected_expression_type = if values.len < targets.len {
targets[values.len].typ
} else {
u32(0)
}
value := g.read_expression([token.Token.comma, token.Token.semicolon, token.Token.rcbr])!
if value == '' {
return g.unsupported('empty parallel assignment')
@@ -2586,6 +2611,7 @@ fn (mut g Parser) parse_parallel_expression_assignment(first_source string, firs
}
g.next()
}
g.expected_expression_type = previous_expected_type
g.consume_statement_end()
if values.len > 1 {
if values.len != targets.len {