ci: fix macOS bootstrap and example builds (#31205)

* fastc: dereference pointer-returning calls with indexed arguments

* builtin: avoid FastC symbol collision in two-byte string search

* tools: build the macOS tray example as a project folder

* tests: align parser and Wasm audits with supported features

* transform: preserve caller types in generic specializations

* cgen: preserve source module identity in cached declarations

* cgen: resolve cached capture array fields in their source module

* test: assert the default SLH-DSA key description
This commit is contained in:
Alexander Medvednikov authored and GitHub committed 2026-10-26 22:30:54 +03:00
1 parent aaa8115298
commit 9db0ba48fa
16 files changed
+249 -44

No files matched your search

+3
View File
@@ -109,6 +109,9 @@ It checkpoints the successful instrumented compiler build. A retry runs the fail
self-compilation without rebuilding `v2`, unless that binary is missing or changed.
Other non-test commands retain their existing task-level retry behavior.
On macOS, `build-examples` builds `examples/macos_tray` as a project folder so its
platform-specific source is included alongside the main program.
A resumed pass is not a fresh validation of earlier tasks or tests against shared
source changes. Compiler and library edits deliberately preserve progress. Use
`ci --reset` for a complete check, after switching branches, or after removing build
+7 -3
View File
@@ -24,14 +24,18 @@ fn main() {
mut requested_flags := vflags.tokenize_to_args(os.getenv('VFLAGS'))
requested_flags << vflags.tokenize_to_args(params)
strict_v3 := '-old-compiler' !in requested_flags
mut skip_prefixes := efolders.map(normalised_vroot_path(it))
res := testing.v_build_failing_skipped(params, 'examples', skip_prefixes, fn [strict_v3] (mut session testing.TestSession) {
mut project_folders := efolders.clone()
$if macos {
project_folders << 'examples/macos_tray'
}
mut skip_prefixes := project_folders.map(normalised_vroot_path(it))
res := testing.v_build_failing_skipped(params, 'examples', skip_prefixes, fn [strict_v3, project_folders] (mut session testing.TestSession) {
if strict_v3 {
// V3 has only a limited JavaScript compatibility generator. Keep full-backend
// examples visible as skips unless the legacy compiler was explicitly selected.
session.skip_files << session.files.filter(it.ends_with('.js.v'))
}
for x in efolders {
for x in project_folders {
pathsegments := x.split_any('/')
fpath := os.real_path(os.join_path(vroot, ...pathsegments))
session.skip_files = session.skip_files.filter(it != fpath)
+2 -2
View File
@@ -1527,7 +1527,7 @@ fn (s string) index_multibyte(p string) int {
return s.index_kmp(p)
}
if p.len == 2 {
return s.index_pair(p)
return s.index_two_bytes(p)
}
// Short needles: scanning for the first byte and comparing the rest in place
// needs no prefix table, and costs at most p.len compares per position.
@@ -1550,7 +1550,7 @@ fn (s string) index_multibyte(p string) int {
}
@[direct_array_access]
fn (s string) index_pair(p string) int {
fn (s string) index_two_bytes(p string) int {
return string_index_pair(s.str, s.len, unsafe { p.str[0] }, unsafe { p.str[1] })
}
+3
View File
@@ -24269,6 +24269,9 @@ fn (mut g FlatGen) fixed_array_typedef_type_module(name string, fallback string)
if info := g.struct_decl_infos[name] {
return info.module
}
if module_name := g.tc.struct_modules[name] {
return module_name
}
mod := module_from_qualified_name(name)
if mod.len > 0 {
return mod
+62
View File
@@ -0,0 +1,62 @@
module c
import v.flat
import v.types
fn test_generated_capture_struct_uses_source_module_for_fixed_array_size() {
mut a := flat.FlatAst.new()
value := a.add_val(.int_literal, '4')
mut tc := types.TypeChecker.new(&a)
tc.cur_module = 'cached_support'
tc.const_exprs['fixed_array_size'] = value
tc.const_modules['fixed_array_size'] = 'main'
context_name := '__anon_fn_0_Ctx'
tc.struct_modules[context_name] = 'main'
tc.structs[context_name] = [types.StructField{
name: 'values'
typ: types.Type(types.Pointer{
base_type: types.Type(types.ArrayFixed{
elem_type: types.Type(types.int_)
len_expr: 'fixed_array_size'
})
})
}]
// Simulate the spelling first requested while generating cached support code.
assert tc.c_type(tc.structs[context_name][0].typ) == 'Array_fixed_i64_fixed_array_size*'
mut g := FlatGen.new()
g.a = &a
g.tc = &tc
g.emit_struct(context_name)
assert g.sb.str().contains('Array_fixed_i64_4* values;'), g.sb.str()
assert tc.cur_module == 'cached_support'
}
fn test_callback_field_lookup_preserves_generic_argument_module() {
mut a := flat.FlatAst.new()
mut tc := types.TypeChecker.new(&a)
tc.cur_module = 'genericmodule'
caller_callback := types.Type(types.FnType{ return_type: types.Type(types.int_) })
imported_callback := types.Type(types.FnType{ return_type: types.Type(types.string_) })
tc.structs['genericmodule.Each[Row]'] = [types.StructField{
name: 'visit'
typ: caller_callback
}]
tc.structs['genericmodule.Each[genericmodule.Row]'] = [types.StructField{
name: 'visit'
typ: imported_callback
}]
mut g := FlatGen.new()
g.a = &a
g.tc = &tc
caller := types.Type(types.Struct{ name_id: flat.source_type_id('genericmodule.Each[main.Row]') })
imported := types.Type(types.Struct{ name_id: flat.source_type_id('genericmodule.Each[genericmodule.Row]') })
assert g.field_type(caller, 'visit')? == caller_callback
assert g.field_type(imported, 'visit')? == imported_callback
assert g.field_type(caller, 'missing') == none
// An exact specialization takes precedence over the bare main spelling.
tc.structs['genericmodule.Each[main.Row]'] = [types.StructField{
name: 'visit'
typ: imported_callback
}]
assert g.field_type(caller, 'visit')? == imported_callback
}
+5 -1
View File
@@ -13546,7 +13546,11 @@ fn (g &FlatGen) field_type(base_type types.Type, field_name string) ?types.Type
if struct_name.len == 0 {
return none
}
fields := g.tc.structs[struct_name] or { return none }
fields := g.tc.structs[struct_name] or {
// Checked generic arguments can retain `main.` to distinguish the caller's
// type from an imported namesake; materialized structs use bare main names.
g.tc.structs[strip_main_module_qualifier(struct_name)] or { return none }
}
for field in fields {
if field.name == field_name {
return field.typ
+5
View File
@@ -8308,6 +8308,11 @@ fn (mut g FlatGen) write_struct_field(_struct_name string, f types.StructField)
g.writeln('\t${qualifier}${ct}* ${g.cname(f.name)};')
return
}
if _ := array_fixed_type(types.unwrap_all_pointers(field_type.base_type)) {
// Cached C spellings can retain a length evaluated in another module.
g.writeln('\t${qualifier}${g.value_c_type(field_type)} ${g.cname(f.name)};')
return
}
}
if field_type is types.FnType {
c_abi_fn := g.struct_field_c_abi_fn_ptr_type(flat.source_type_id(_struct_name), f.name) or {
@@ -0,0 +1,61 @@
module fastc
import os
import v.cmdexec
fn test_fastc_pointer_call_with_indexed_arguments_is_passed_by_value() {
$if !macos && !linux {
return
}
root := os.join_path(os.vtmp_dir(), 'fastc_indexed_call_argument_${os.getpid()}')
os.mkdir_all(root) or { panic(err) }
defer { os.rmdir_all(root) or {} }
source := os.join_path(root, 'main.v')
os.write_file(source, r'module main
struct Record {
value int
}
type Value = Record | int
fn record(value int) &Record { return &Record{value: value} }
fn boxed(value &Value, expected int) &Value { assert expected == 42; return value }
fn read_record(value Record) int { return value.value }
fn read_value(value Value) int {
return match value {
Record { value.value }
int { value }
}
}
fn identity(value int) int { return value }
fn next(mut calls int) int { calls++; return 0 }
fn slice_len(values []int) int { return values.len }
fn forward(mut values []int) int { return slice_len(values[1..]) }
fn grouped_forward(mut values []int) int { return slice_len((values[1..])) }
fn main() {
mut values := [42, 43]
mut calls := 0
assert read_record(record(values[next(mut calls)])) == 42
assert calls == 1
value := Value(Record{value: 42})
assert read_value(boxed(&value, identity(values[next(mut calls)]))) == 42
assert calls == 2
assert read_record((record(values[next(mut calls)]))) == 42
assert calls == 3
assert forward(mut values) == 1
assert grouped_forward(mut values) == 1
}
') or { panic(err) }
program := os.join_path(root, 'program')
build := cmdexec.run_in_merged_with_env(@VEXE, ['-b', 'fastc', '-d', 'fastc_real_builtin',
'-nocache', '-gc', 'none', '-o', program, source], @VEXEROOT, {
'VEXE': @VEXE
'VFLAGS': ''
'VOSARGS': ''
'V_MACOS_V3_NO_FALLBACK': '1'
'V_C_ERROR_BUG_REPORT_DISABLED': '1'
})
assert build.exit_code == 0, build.output
run := cmdexec.run(program, [])
assert run.exit_code == 0, run.output
}
+2 -1
View File
@@ -4932,11 +4932,12 @@ fn (g &Parser) render_call_argument_expression(tokens []FastcExpressionToken, ex
// The `.lsbr` guard below would wrongly skip this because the chain indexes an array.
return '*(${rendered})'
}
if flat.source_type_query(actual_type).has(.c_pointer_suffix) && expected_type_source_id == fastc_trim_pointer_type(actual_type) && expected_type_source_id != flat.source_type_voidptr && !fastc_expression_tokens_contain(tokens, .lsbr) {
if flat.source_type_query(actual_type).has(.c_pointer_suffix) && expected_type_source_id == fastc_trim_pointer_type(actual_type) && expected_type_source_id != flat.source_type_voidptr && (!fastc_expression_tokens_contain(tokens, .lsbr) || fastc_tokens_are_plain_call(fastc_strip_paren_tokens(tokens))) {
// V automatically dereferences an explicit reference when a by-value
// parameter is expected (`s &string` passed to `log_line(s string)`). A
// slice of a mutable array is already a by-value array even though the
// receiver local retains its pointer type during inference.
// Indexes inside call arguments do not change the call's pointer result.
return '*(${rendered})'
}
if flat.source_type_query(expected_type_source_id).has(.c_pointer_suffix) && actual_type == flat.source_type_voidptr && rendered.trim_space() == 'NULL' {
+16 -20
View File
@@ -701,43 +701,39 @@ fn main() {
refuses: 'ssa: unknown function `f64__strg`'
},
StubCase{
tag: 'string_split'
stub: 'strings.split - no synthetic body, source body panics'
source: '
tag: 'string_split'
stub: 'strings.split - source body'
source: '
fn main() {
sp := "a,b,c".split(",")
println(sp)
println(sp.len)
}
'
native: "['a', 'b', 'c']\n3\n"
refuses: 'ssa: unknown function `split`'
native: "['a', 'b', 'c']\n3\n"
},
StubCase{
tag: 'string_repeat'
stub: 'strings.repeat - no synthetic body, source body panics'
source: '
tag: 'string_repeat'
stub: 'strings.repeat - source body'
source: '
fn main() {
s := "ab".repeat(3)
println(s)
}
'
native: 'ababab\n'
refuses: 'ssa: unknown function `string__repeat`'
native: 'ababab\n'
},
StubCase{
tag: 'string_index'
stub: 'strings.index - no synthetic body, source body panics'
source: stub_source_string_index
native: 'Option(2)\nOption(none)\n'
refuses: 'ssa: unknown function `index`'
tag: 'string_index'
stub: 'strings.index - source body'
source: stub_source_string_index
native: 'Option(2)\nOption(none)\n'
},
StubCase{
tag: 'string_reverse'
stub: 'strings.reverse - no synthetic body, source body panics'
source: stub_source_string_reverse
native: 'cba\n399\n'
refuses: 'ssa: unknown function `reverse`'
tag: 'string_reverse'
stub: 'strings.reverse - source body'
source: stub_source_string_reverse
native: 'cba\n399\n'
},
StubCase{
tag: 'pointer_strings'
-14
View File
@@ -2,20 +2,6 @@ module parser
import v.flat
fn test_vml_declared_type_ids_keep_exact_spelling_dispatch() {
for type_id in [flat.source_type_f64, flat.source_type_f32, flat.source_type_int] {
assert vml_property_use(VmlProperty{ expr: &VmlExpr{}, declared_type: type_id }) == .number
}
assert vml_property_use(VmlProperty{ expr: &VmlExpr{}, declared_type: flat.source_type_bool }) == .bool_
assert vml_property_use(VmlProperty{ expr: &VmlExpr{}, declared_type: flat.source_type_string }) == .string_
assert vml_property_use(VmlProperty{ expr: &VmlExpr{}, declared_type: flat.source_type_id('color') }) == .color
// Explicit unknown or padded annotations override the name-based inference.
for spelling in [' int ', 'pkg.int', 'unknown'] {
assert vml_property_use(VmlProperty{ expr: &VmlExpr{}, name: 'width', declared_type: flat.source_type_id(spelling) }) == .raw
}
assert vml_property_use(VmlProperty{ expr: &VmlExpr{}, name: 'width' }) == .number
}
fn test_named_constraint_ids_preserve_sparse_parameter_order() {
constraint := flat.source_type_id('pkg.Box[&Item]')
assert named_constraints([u32(0), constraint]) == [u32(0), constraint]
+5 -2
View File
@@ -12635,7 +12635,8 @@ fn (mut t Transformer) clone_generic_node_from(node flat.Node, args_ids []u32, i
}
mut cloned_call_args := []u32{cap: node.call_type_args().len}
for arg in node.call_type_args() {
cloned_call_args << t.subst_type(arg, args_ids)
substituted_arg := t.subst_type(arg, args_ids)
cloned_call_args << t.lock_colliding_main_substitution_type_text(arg, substituted_arg, t.cur_module, t.active_generic_params)
}
has_source_value := node.source_value != 0 || node.kind in [.struct_init, .assoc, .cast_expr,
.as_expr, .is_expr, .map_init, .array_init, .array_literal, .sizeof_expr, .typeof_expr,
@@ -13339,7 +13340,9 @@ fn (mut t Transformer) substitute_cloned_generic_call_type_args(node flat.Node,
&& !generic_text_contains_param(arg, t.active_generic_params) {
continue
}
substituted := flat.source_type_name(t.resolve_substituted_type_text(t.subst_type(arg_source_id_10, args_ids)))
concrete := t.subst_type(arg_source_id_10, args_ids)
locked := t.lock_colliding_main_substitution_type_text(arg_source_id_10, concrete, t.cur_module, t.active_generic_params)
substituted := flat.source_type_name(t.resolve_substituted_type_text(locked))
if substituted.len == 0 || substituted == arg
|| t.generic_args_have_placeholders(flat.TypeTextList.from_names([substituted]).ids) {
continue
+21
View File
@@ -1365,3 +1365,24 @@ type Choice = int | string
}
}
}
fn test_forwarded_generic_call_keeps_caller_context_identity() {
mut a := flat.FlatAst.new()
callee := a.add_node(flat.Node{ kind: .ident, value: 'forward' })
arg := a.add_node(flat.Node{ kind: .ident, value: 'T' })
start := a.children.len
a.children << callee
a.children << arg
index := flat.Node{ kind: .index, children_start: start, children_count: 2 }
mut tc := types.TypeChecker.new(&a)
tc.structs['Context'] = []types.StructField{}
tc.structs['callee.Context'] = []types.StructField{}
mut t := new_transformer(mut a, &tc, map[string]bool{})
t.cur_module = 'callee'
t.active_generic_params = ['T']
t.structs['Context'] = StructInfo{}
t.structs['callee.Context'] = StructInfo{ module: 'callee' }
mut children := [callee, arg]
t.substitute_cloned_generic_call_type_args(index, mut children, [flat.source_type_id('Context')])
assert t.a.node(children[1]).value_text() == 'main.Context'
}
+12
View File
@@ -7090,6 +7090,18 @@ fn (tc &TypeChecker) qualify_type_text_impl(typ string, resolution bool, generic
}
}
if resolution && clean.contains('.') {
// Keep concrete caller types locked while resolving a foreign generic body.
// Generic bases retain their canonical specialization spelling below.
if tc.cur_module != 'main' && tc.cur_module.len > 0
&& clean.starts_with('main.') && !clean['main.'.len..].contains('.') {
base := clean['main.'.len..]
if _ := tc.resolve_import_alias('main') {
} else if base !in tc.struct_generic_params && base !in tc.sum_generic_params
&& base !in tc.interface_generic_params && base !in tc.type_alias_generic_params
&& tc.qualify_candidate_type_exists(base) {
return clean
}
}
resolved := flat.source_type_name(tc.resolve_imported_type_text(flat.source_type_id(clean)))
if resolved != clean {
return resolved
@@ -2,6 +2,31 @@ module types
import v.flat
fn test_generic_receiver_field_keeps_explicit_main_argument() {
a := flat.FlatAst.new()
mut tc := TypeChecker.new(&a)
tc.cur_module = 'foreign'
tc.cur_file = 'foreign.v'
tc.structs['Record'] = []StructField{}
tc.structs['foreign.Record'] = []StructField{}
tc.struct_generic_params['foreign.Slot'] = ['T']
tc.structs['foreign.Slot'] = [StructField{
name: 'value'
typ: unknown_type('generic placeholder `T`')
}]
assert tc.qualified_generic_suffix([flat.source_type_id('main.Record')]) == '[main.Record]'
receiver := tc.parse_scope_param_type(flat.source_type_id('&foreign.Slot[main.Record]'))
assert receiver.name() == '&foreign.Slot[main.Record]'
field := tc.struct_field_type(flat.source_type_id('foreign.Slot[main.Record]'), 'value') or {
panic('missing generic field')
}
assert field.name() == 'Record'
// A real import named main must still resolve through that import.
tc.register_file_import('main', 'other')
tc.structs['other.Record'] = []StructField{}
assert tc.qualified_generic_suffix([flat.source_type_id('main.Record')]) == '[other.Record]'
}
fn test_qualified_generic_argument_ids_keep_nested_commas_and_file_imports() {
a := flat.FlatAst.new()
mut tc := TypeChecker.new(&a)
@@ -42,3 +67,22 @@ fn test_generic_alias_argument_ids_keep_substitution_order_and_arity_fallback()
assert fallback.base_type.source_type_id() == flat.source_type_u64
}
}
fn test_main_generic_base_keeps_canonical_specialization_spelling() {
a := flat.FlatAst.new()
mut tc := TypeChecker.new(&a)
tc.cur_module = 'foreign'
tc.struct_generic_params['Box'] = ['T']
tc.structs['Box'] = []StructField{}
tc.struct_generic_params['foreign.Box'] = ['T']
tc.structs['foreign.Box'] = []StructField{}
assert tc.qualified_generic_suffix([flat.source_type_id('main.Box[string]')]) == '[Box[string]]'
}
fn test_main_callback_type_keeps_unqualified_diagnostic_spelling() {
a := flat.FlatAst.new()
mut tc := TypeChecker.new(&a)
tc.cur_module = 'main'
tc.structs['Context'] = []StructField{}
assert flat.source_type_name(tc.qualify_resolution_type_text(flat.source_type_id('fn (mut main.Context) bool'))) == 'fn(mut Context) bool'
}
+1 -1
View File
@@ -73,7 +73,7 @@ fn test_privkey_signing() ! {
fn test_basic_properties() ! {
mut pv := PrivateKey.new()!
desc := key_description(pv.key)
unsafe { desc.vstring() } == 'OpenSSL SLH-DSA-SHA2-128f implementation'
assert unsafe { desc.vstring() } == 'OpenSSL SLH-DSA-SHA2-128s implementation'
n := C.EVP_PKEY_is_a(pv.key, ln_slhdsa_sha2_128s)
assert n == 1 // true