Fix macOS CI with default caching and integer migration (#31154)

* cgen: scope cleanup switch cases for older Apple Clang

* checker: let zero assignments leave reference sum variants

* math.big: pass signed byte counts to memory helpers

* checker: limit zero literal inspection to reference narrowing

* checker: stop loop narrowing scans before the current assignment

* tests: repair integer migration inputs for macOS self tests

* checker: return platform int for translated pointer differences

* examples: use signed byte counts for graphics buffers

* cgen: preserve headerless mmap bindings in cached modules

* fastc: repair pointer difference formatting and bootstrap fallback
This commit is contained in:
Alexander Medvednikov authored and GitHub committed 2026-10-26 05:23:57 +03:00
1 parent b4a41308ec
commit 8bd7ffa3f8
19 files changed
+194 -34

No files matched your search

+9
View File
@@ -113,6 +113,15 @@ source changes. Compiler and library edits deliberately preserve progress. Use
artifacts needed by later tasks. Do not run multiple `ci` jobs in the same checkout
concurrently; they share the checkpoint and build outputs.
The macOS self-tests use default module caching. Generated cleanup for detached
thread results must also compile with older Apple Clang versions that reject
declarations immediately after `case` labels. The focused regression checks
uncached, cold-cache and warm-cache builds, including nested error payloads:
```sh
./v test vlib/v/gen/c/detached_spawn_cleanup_test.v
```
### Testing the resume behavior
On macOS or Linux with a working V compiler, run:
+3
View File
@@ -739,6 +739,7 @@ voidptr // this one is mostly used for [C interoperability](#v-and-c)
`isize` and `usize` are deprecated. Replace `isize` with `int` and `usize` with `uint`.
Use `uint` for pointer addresses and C `size_t` values to preserve
the full unsigned range. Existing spellings still compile with deprecation warnings.
Pointer subtraction returns `int`, including in translated C code.
### 128-bit integers
@@ -5174,6 +5175,8 @@ fixed-array elements.
A mutable sum argument requires a variable of the sum type: it can replace the complete sum
with another variant. Wrap a concrete value in a sum variable before passing it with `mut`.
Inside a mutable smartcast, modifying a field modifies the selected payload in that sum.
Assigning `0` to a sum with integer and reference variants selects the integer variant,
including when its current variant is a reference.
Sum payloads must have finite layout. References, dynamic arrays, and maps break recursive
storage cycles; ordinary fields, options, and fixed arrays do not. A function variant stores
+2 -2
View File
@@ -32,8 +32,8 @@ struct AppState {
mut:
gg &gg.Context = unsafe { nil }
iidx int
pixels &u32 = unsafe { &u32(vcalloc(pwidth * pheight * sizeof(u32))) }
npixels &u32 = unsafe { &u32(vcalloc(pwidth * pheight * sizeof(u32))) } // all drawing happens here, results are swapped at the end
pixels &u32 = unsafe { &u32(vcalloc(pwidth * pheight * int(sizeof(u32)))) }
npixels &u32 = unsafe { &u32(vcalloc(pwidth * pheight * int(sizeof(u32)))) } // all drawing happens here, results are swapped at the end
view ViewRect = ViewRect{-3.0773593290970673, 1.4952456603855397, -2.019938598189011, 2.3106642054225945}
scale int = 1
ntasks int = runtime.nr_jobs()
+1 -1
View File
@@ -17,7 +17,7 @@ mut:
@[direct_array_access]
fn (mut state AppState) update() {
for {
unsafe { vmemset(&state.pixels, 0, pwidth * pheight * sizeof(gg.Color)) }
unsafe { vmemset(&state.pixels, 0, pwidth * pheight * int(sizeof(gg.Color))) }
state.draw_sky() or {}
time.sleep(30_000 * time.millisecond)
}
+2 -2
View File
@@ -291,8 +291,8 @@ fn left_shift_digits_in_place(mut a []u64, amount int) {
a.grow_len(amount)
sptr := &u8(a.data)
dptr := &u8(a.data) + u64(amount) * u64(elem_size)
vmemmove(dptr, sptr, u64(old_len) * u64(elem_size))
vmemset(sptr, 0, u64(amount) * u64(elem_size))
vmemmove(dptr, sptr, old_len * elem_size)
vmemset(sptr, 0, amount * elem_size)
}
}
+7
View File
@@ -6057,6 +6057,13 @@ fn (g &FlatGen) core_native_header_is_redeclared(source_file string, include_arg
return false
}
relative := path[root.len + 6..]
// Cached module headers stand in for every source, but their owner can be
// any file in that module. Keep os's mmap declarations headerless on warm builds.
if !isnil(g.a) && source_file in g.a.cached_header_sources
&& relative.starts_with('os/') && include_arg == '<sys/mman.h>' {
return g.target.pointer_bits == 64 && g.target.os in ['macos', 'linux', 'android', 'termux',
'freebsd', 'openbsd', 'dragonfly']
}
if relative.starts_with('term/termios/termios_') && relative.ends_with('.c.v') {
return include_arg in ['<termios.h>', '<sys/ioctl.h>']
}
@@ -1,8 +1,49 @@
module c
import os
import v.cmdexec
import v.flat
import v.types
fn test_detached_spawn_nested_error_cleanup_compiles_as_c11() ! {
cc := os.find_abs_path_of_executable('clang') or { return }
dir := os.join_path(os.vtmp_dir(), 'detached_error_cleanup_${os.getpid()}')
os.mkdir_all(dir)!
defer { os.rmdir_all(dir) or {} }
source := os.join_path(dir, 'main.v')
os.write_file(source, 'struct NestedError { cause IError }
fn (e NestedError) msg() string { return e.cause.msg() }
fn (e NestedError) code() int { return 1 }
interface Container { cause IError }
type Choice = NestedError | int
fn fail() ! { return NestedError{cause: error("nested")} }
fn boxed() Container { return NestedError{cause: error("boxed")} }
fn choice() Choice { return NestedError{cause: error("choice")} }
fn main() {
spawn fail()
spawn boxed()
spawn choice()
}
')!
// Modern Clang accepts declarations after case labels as a C23 extension;
// older macOS CI compilers reject them. Exercise uncached, cold and warm builds.
for mode in ['uncached', 'cold', 'warm'] {
mut args := ['-cc', cc, '-cstrict', '-show-c-output', '-cflags',
'-std=gnu11 -Wc23-extensions -Wno-unknown-warning-option', '-o',
os.join_path(dir, 'program')]
if mode == 'uncached' {
args << '-nocache'
}
args << source
result := cmdexec.run_in_merged_with_env(@VEXE, args, dir, {
'V3CACHE': os.join_path(dir, 'cache')
'VJOBS': '1'
})
assert result.exit_code == 0, '${mode}: ${result.output}'
assert !result.output.contains('label followed by a declaration'), '${mode}: ${result.output}'
}
}
fn test_detached_spawn_cleanup_reaches_deep_thread_results() {
mut ast := flat.FlatAst.new()
mut tc := types.TypeChecker.new(&ast)
+7
View File
@@ -30,6 +30,13 @@ fn test_mmap_header_ownership_is_limited_to_supported_shipped_bindings() {
a.cached_header_sources['/cache/os.vh'] = source
g.a = &a
assert g.core_native_header_is_redeclared('/cache/os.vh', '<sys/mman.h>')
a.cached_header_sources['/cache/os.vh'] = '/vroot/vlib/os/args.v'
assert g.core_native_header_is_redeclared('/cache/os.vh', '<sys/mman.h>')
a.cached_header_sources['/cache/user.vh'] = '/project/os/args.v'
assert !g.core_native_header_is_redeclared('/cache/user.vh', '<sys/mman.h>')
g.set_target(pref.target_from('linux', 'arm32')!)
assert !g.core_native_header_is_redeclared('/cache/os.vh', '<sys/mman.h>')
g.set_target(pref.target_from('macos', 'arm64')!)
g.no_builtin = true
assert !g.core_native_header_is_redeclared(source, '<sys/mman.h>')
g.no_builtin = false
+7 -3
View File
@@ -1738,11 +1738,12 @@ fn (mut g FlatGen) gen_ownership_drop_value_inner(typ types.Type, expr string, d
continue
}
concrete_ct := g.value_c_type(concrete_type)
g.writeln('case ${id}:')
g.writeln('case ${id}: {')
g.indent++
g.gen_ownership_drop_value_inner(concrete_type, '*((${concrete_ct}*)${object})', depth + 1, mut expanding)
g.writeln('break;')
g.indent--
g.writeln('}')
}
g.writeln('default: break;')
g.writeln('}')
@@ -1760,7 +1761,7 @@ fn (mut g FlatGen) gen_ownership_drop_value_inner(typ types.Type, expr string, d
resolved_variant := g.resolve_variant(sum_name_id, variant)
variant_type := g.tc.parse_canonical_type_cached(resolved_variant)
idx := g.sum_type_index(sum_name_id, resolved_variant)
g.writeln('case ${idx}:')
g.writeln('case ${idx}: {')
g.indent++
clean_variant_type := unalias_type(variant_type)
if clean_variant_type is types.Pointer {
@@ -1774,6 +1775,7 @@ fn (mut g FlatGen) gen_ownership_drop_value_inner(typ types.Type, expr string, d
g.writeln('}')
g.writeln('break;')
g.indent--
g.writeln('}')
continue
}
field := sum_field_name(flat.source_type_name(resolved_variant))
@@ -1781,6 +1783,7 @@ fn (mut g FlatGen) gen_ownership_drop_value_inner(typ types.Type, expr string, d
g.gen_ownership_drop_value_inner(variant_type, payload, depth + 1, mut expanding)
g.writeln('break;')
g.indent--
g.writeln('}')
}
g.writeln('default: break;')
g.writeln('}')
@@ -1813,7 +1816,7 @@ fn (mut g FlatGen) gen_ownership_drop_result_error(expr string, depth int, mut e
continue
}
concrete_ct := g.value_c_type(concrete_type)
g.writeln('case ${id}:')
g.writeln('case ${id}: {')
g.indent++
if concrete in ['MessageError', 'builtin.MessageError'] {
// MessageError is introduced by generated result/error lowering and can be
@@ -1824,6 +1827,7 @@ fn (mut g FlatGen) gen_ownership_drop_result_error(expr string, depth int, mut e
}
g.writeln('break;')
g.indent--
g.writeln('}')
}
g.writeln('default: break;')
g.writeln('}')
+4 -2
View File
@@ -185,6 +185,8 @@ fn main() {
assert null is &Item
assert voidptr(null as &Item) == unsafe { nil }
}')!
result := os.exec([@VEXE, '-new-compiler', '-nocache', '-no-closures', 'run', path])
assert result.exit_code == 0, result.output
for flags in [['-nocache'], []string{}] {
result := os.exec([@VEXE, '-new-compiler', ...flags, '-no-closures', 'run', path])
assert result.exit_code == 0, result.output
}
}
+27
View File
@@ -271,3 +271,30 @@ fn main() {
assert !source.contains('extern ptrdiff_t write(i32, const void*'), source
declaration_abi_run_c('target_write', source)
}
fn test_fastc_interpolates_native_pointer_differences() {
$if !macos && !linux {
return
}
mut prefs := pref.new_preferences()
prefs.vroot = @VEXEROOT
program := r"module main
@[c_extern]
fn C.native_difference() C.ptrdiff_t
fn main() {
difference := C.native_difference()
assert '${difference}' == '-15'
assert '${difference:05d}' == '-0015'
}
"
for selfhost in [false, true] {
prefs.target_libc_headers = selfhost
source := if selfhost {
declaration_abi_generate('ptrdiff', program, prefs)
} else {
generate(program, 'ptrdiff.c.v', prefs) or { panic(err) }
}
assert source.contains('extern ptrdiff_t native_difference('), source
declaration_abi_run_c('ptrdiff_${selfhost}', '#include <stddef.h>\n' + source + '\nptrdiff_t native_difference(void) { return -15; }\n')
}
}
+1 -1
View File
@@ -2239,7 +2239,7 @@ fn (mut g Parser) parse_parallel_assignment(initial_names []string, initial_mut
component_type := if i < component_types.len {
flat.source_type_name(component_types[i])
} else {
'uint', 'usize'
'uint'
}
c_name := fastc_c_identifier(name)
c_type := fastc_output_c_type(component_type)
+5 -1
View File
@@ -492,11 +492,15 @@ fn (mut g Parser) read_interpolated_string() !string {
value_type := fastc_normalize_inferred_type(g.last_expression_type)
value_key := g.semantic_type_key(value_type)
alias_has_str_method := g.declared_kinds[flat.source_type_name(value_key)] == .alias_ && '${flat.source_type_name(value_key)}.str' in g.functions
interpolation_type := if alias_has_str_method {
mut interpolation_type := if alias_has_str_method {
flat.source_type_name(value_type)
} else {
flat.source_type_name(g.underlying_alias_type(value_type))
}
// C pointer differences use the signed pointer-sized integer formatter.
if interpolation_type == 'ptrdiff_t' {
interpolation_type = 'isize'
}
mut format_specifier := ''
if g.tok == .colon {
g.next()
@@ -22,11 +22,11 @@ fn test_stringified_usize_field_should_be_always_positive() {
assert sa.contains('f_isize: -1')
assert sa.contains('f_i64: -1')
assert sa.contains('f_i32: -1')
i := sa.split_into_lines().filter(it.contains('int'))[0]
i := sa.split_into_lines().filter(it.contains('f_isize:'))[0]
assert i.contains('-'), 'all `i` fields should be negative, but ${i} != ${a.f_isize}'
assert sa.contains('f_u64: 18446744073709551615')
assert sa.contains('f_u32: 4294967295')
u := sa.split_into_lines().filter(it.contains('uint'))[0]
u := sa.split_into_lines().filter(it.contains('f_usize:'))[0]
assert !u.contains('-'), 'all `u` fields should be positive, but ${u} != ${a.f_usize}'
}
@@ -2,7 +2,7 @@ interface Number2 {}
fn t(v Number2) {
match v {
int, i32, int, i64 {
int, i32, i64 {
x := int(v)
println(x)
assert x == 42
@@ -22,4 +22,6 @@ fn t(v Number2) {
fn test_cast_interface_value_in_match() {
t(int(42))
t(i32(42))
t(i64(42))
}
@@ -215,6 +215,10 @@ fn test_translated_pointer_subtraction_retains_pointer_width() {
delta := unsafe { end - start }
assert typeof(delta).name == 'int'
assert delta == 3
assert sizeof(delta) == sizeof(voidptr)
reverse := unsafe { start - end }
assert typeof(reverse).name == 'int'
assert reverse == -3
}
fn translated_mixed_branch(flag bool) int {
+5
View File
@@ -1067,6 +1067,11 @@ fn (tc &TypeChecker) assignment_preserves_smartcast(op flat.Op, lhs_id flat.Node
// type (`x = 5` in `if mut x is int`), as in V 0.5.2. Any other value may select
// another variant, so the narrowing ends.
fn (tc &TypeChecker) assigned_value_keeps_narrowing(rhs_id flat.NodeId, rhs_type &Type, narrowed &Type) bool {
// An untyped zero selects a numeric variant rather than a null pointer.
if (narrowed is Pointer || (narrowed is Alias && narrowed.base_type is Pointer))
&& tc.zero_literal_can_be_pointer(rhs_id, narrowed) {
return false
}
return tc.expr_compatible(rhs_id, rhs_type, narrowed)
|| tc.pointer_value_compatible(rhs_type, narrowed)
}
+25 -19
View File
@@ -16564,12 +16564,13 @@ fn (tc &TypeChecker) lexical_smartcast_type_in_parents(id flat.NodeId, key strin
// The loop condition re-narrows `key` at the top of every iteration, but a
// write to `key` earlier in the same iteration drops that narrowing for the
// rest of the body (the dynamic pass deletes the smartcast on assignment).
// Only reconstruct the condition smartcast when no preceding body statement,
// nor the statement holding `id`, writes `key`.
// Only reconstruct the condition smartcast when no statement before `id`
// in this iteration writes `key`.
if body_index >= 3 && tc.condition_may_smartcast_key(tc.a.child(parent, 1), key) {
for binding in tc.extract_smartcasts(tc.a.child(parent, 1)) {
if binding.name == key
&& !tc.for_body_writes_key_before(parent, body_index, current, key, binding.typ) {
&& !tc.for_body_writes_key_before(parent, body_index, current, id, key,
binding.typ) {
return binding.typ
}
}
@@ -16604,18 +16605,19 @@ fn (tc &TypeChecker) lexical_smartcast_type_in_parents(id flat.NodeId, key strin
return none
}
// for_body_writes_key_before reports whether any loop-body statement up to and
// including `current` (the statement holding the checked node) ends the narrowing
// of `key` to `narrowed` (see subtree_ends_narrowing). Such a write invalidates the
// loop-condition narrowing for the rest of that iteration, so the lexical fallback
// must not restore it.
fn (tc &TypeChecker) for_body_writes_key_before(parent flat.Node, body_index int, current flat.NodeId, key string, narrowed Type) bool {
// for_body_writes_key_before reports whether any loop-body statement before
// the checked `target` within `current` ends the narrowing of `key` to `narrowed`
// (see subtree_ends_narrowing). Such a write invalidates the loop-condition
// narrowing for the rest of that iteration, so the lexical fallback must not restore it.
fn (tc &TypeChecker) for_body_writes_key_before(parent flat.Node, body_index int, current flat.NodeId, target flat.NodeId, key string, narrowed Type) bool {
for i in 3 .. body_index {
if tc.subtree_ends_narrowing(tc.a.child(parent, i), key, narrowed, true) {
return true
}
}
return tc.subtree_ends_narrowing(current, key, narrowed, true)
// The current assignment has not stored its value while its RHS is resolved.
// Resolving it here would query the same loop smartcast recursively.
return tc.subtree_writes_key_before(current, target, key, narrowed, true)
}
// branch_writes_key_before reports whether a statement that runs before `target`
@@ -16633,15 +16635,23 @@ fn (tc &TypeChecker) branch_writes_key_before(branch_id flat.NodeId, target flat
if branch.kind !in [.block, .match_branch] {
return false
}
calls := branch.kind == .block
return tc.subtree_writes_key_before(branch_id, target, key, narrowed, branch.kind == .block)
}
fn (tc &TypeChecker) subtree_writes_key_before(root flat.NodeId, target flat.NodeId, key string, narrowed Type, calls bool) bool {
root_kind := tc.a.node(root).kind
mut child := target
if child == root {
return false
}
for _ in 0 .. 128 {
parent_id := tc.direct_parent_id(child)
if !tc.valid_node_id(parent_id) {
return false
}
if parent_id == branch_id {
return tc.statements_before_end_narrowing(parent_id, child, key, narrowed, calls)
if parent_id == root {
return root_kind in [.block, .match_branch]
&& tc.statements_before_end_narrowing(parent_id, child, key, narrowed, calls)
}
if tc.a.node(parent_id).kind in [.block, .match_branch, .for_stmt, .for_in_stmt, .lock_expr,
.defer_stmt, .select_branch]
@@ -16650,7 +16660,7 @@ fn (tc &TypeChecker) branch_writes_key_before(branch_id flat.NodeId, target flat
}
child = parent_id
}
return tc.statements_before_end_narrowing(branch_id, flat.empty_node, key, narrowed, calls)
return tc.statements_before_end_narrowing(root, flat.empty_node, key, narrowed, calls)
}
// statements_before_end_narrowing reports whether a child of `parent_id` before
@@ -20036,11 +20046,7 @@ fn (tc &TypeChecker) resolve_type_nonliteral(id flat.NodeId) Type {
}
if node.op in [.plus, .minus] {
if node.op == .minus && lt is Pointer && rt is Pointer {
return if tc.node_is_in_translated_file(id) {
builtin_isize_type
} else {
builtin_int_type
}
return builtin_int_type
}
if lt is Pointer && (rt_raw.is_integer()
|| (tc.node_is_in_translated_file(id) && translated_integer_type(rt_raw))) {
+39
View File
@@ -146,6 +146,45 @@ fn test_lexical_parent_cache_forks_own_slots_and_phase_resets_drop_storage() {
assert isnil(forked.lexical_parent_memo)
}
fn test_loop_narrowing_resolves_assignment_rhs_before_its_write() {
mut a := flat.FlatAst.new()
before := a.add_val(.ident, 'value')
lhs := a.add_val(.ident, 'value')
rhs_value := a.add_val(.ident, 'value')
one := a.add_val(.int_literal, '1')
rhs := lexical_parent_cache_children(mut a, .infix, [rhs_value, one])
a.nodes[int(rhs)].op = .plus
write := lexical_parent_cache_children(mut a, .assign, [lhs, rhs])
a.nodes[int(write)].op = .assign
after_int := a.add_val(.ident, 'value')
other_lhs := a.add_val(.ident, 'value')
other_rhs := a.add_val(.string_literal, 'changed')
other_write := lexical_parent_cache_children(mut a, .assign, [other_lhs, other_rhs])
a.nodes[int(other_write)].op = .assign
after_string := a.add_val(.ident, 'value')
nested := lexical_parent_cache_children(mut a, .block, [other_write, after_string])
cond := lexical_parent_cache_condition(mut a, 'value', 'int')
init := a.add(.empty)
post := a.add(.empty)
loop := lexical_parent_cache_children(mut a, .for_stmt, [init, cond, post, before, write,
after_int, nested])
lexical_parent_cache_children(mut a, .fn_decl, [loop])
for cached in [false, true] {
mut tc := TypeChecker.new(&a)
tc.cache_lexical_parents = cached
tc.build_direct_parent_index(&a)
tc.sum_types.set('Value', ['int', 'string'])
sum := Type(SumType{ name_id: flat.source_type_id('Value') })
tc.cur_scope.insert('value', sum)
tc.remember_expr_type(lhs, sum)
tc.remember_expr_type(other_lhs, sum)
assert tc.lexical_smartcast_type_in_parents(before, 'value', false)? == Type(int_)
assert tc.lexical_smartcast_type_in_parents(rhs_value, 'value', false)? == Type(int_)
assert tc.lexical_smartcast_type_in_parents(after_int, 'value', false)? == Type(int_)
assert tc.lexical_smartcast_type_in_parents(after_string, 'value', false) == none
}
}
fn test_lexical_parent_cache_matches_uncached_reachability_and_diagnostics() {
path := os.join_path(os.vtmp_dir(), 'v3_lexical_parent_differential_${os.getpid()}.v')
os.write_file(path, 'module main