From 8bd7ffa3f8a1ef4475ff7cc2ec9c3bf5c84dc830 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Mon, 26 Oct 2026 05:23:57 +0300 Subject: [PATCH] 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 --- ci/README.md | 9 ++++ doc/docs.md | 3 ++ examples/gg/mandelbrot.v | 4 +- examples/gg/random_stars.v | 2 +- vlib/math/big/special_array_ops.v | 4 +- vlib/v/gen/c/cleanc.v | 7 +++ vlib/v/gen/c/detached_spawn_cleanup_test.v | 41 +++++++++++++++++ vlib/v/gen/c/mmap_headerless_test.v | 7 +++ vlib/v/gen/c/stmt.v | 10 +++-- vlib/v/gen/c/sumtype_semantics_test.v | 6 ++- vlib/v/gen/fastc/declaration_abi_test.v | 27 ++++++++++++ vlib/v/gen/fastc/stmt.v | 2 +- vlib/v/gen/fastc/str_intp.v | 6 ++- ...terpolation_struct_with_usize_field_test.v | 4 +- .../cast_interface_value_in_match_test.v | 4 +- .../tests/translated_scalar_promotions_test.v | 4 ++ vlib/v/types/checker_tail.v | 5 +++ vlib/v/types/checker_tail_stmt.v | 44 +++++++++++-------- vlib/v/types/lexical_parent_cache_test.v | 39 ++++++++++++++++ 19 files changed, 194 insertions(+), 34 deletions(-) diff --git a/ci/README.md b/ci/README.md index 8f4be21c7e..0aea98c083 100644 --- a/ci/README.md +++ b/ci/README.md @@ -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: diff --git a/doc/docs.md b/doc/docs.md index e094eaf0cf..fafa2115f6 100644 --- a/doc/docs.md +++ b/doc/docs.md @@ -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 diff --git a/examples/gg/mandelbrot.v b/examples/gg/mandelbrot.v index d9d65c452d..45309040a4 100644 --- a/examples/gg/mandelbrot.v +++ b/examples/gg/mandelbrot.v @@ -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() diff --git a/examples/gg/random_stars.v b/examples/gg/random_stars.v index 39f4a12a0b..9c8036c46a 100644 --- a/examples/gg/random_stars.v +++ b/examples/gg/random_stars.v @@ -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) } diff --git a/vlib/math/big/special_array_ops.v b/vlib/math/big/special_array_ops.v index dc3f261ce4..d20313951e 100644 --- a/vlib/math/big/special_array_ops.v +++ b/vlib/math/big/special_array_ops.v @@ -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) } } diff --git a/vlib/v/gen/c/cleanc.v b/vlib/v/gen/c/cleanc.v index d2a77fd5af..d318110b9f 100644 --- a/vlib/v/gen/c/cleanc.v +++ b/vlib/v/gen/c/cleanc.v @@ -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 == '' { + 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 ['', ''] } diff --git a/vlib/v/gen/c/detached_spawn_cleanup_test.v b/vlib/v/gen/c/detached_spawn_cleanup_test.v index fff86a0bf1..9bb35d57b9 100644 --- a/vlib/v/gen/c/detached_spawn_cleanup_test.v +++ b/vlib/v/gen/c/detached_spawn_cleanup_test.v @@ -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) diff --git a/vlib/v/gen/c/mmap_headerless_test.v b/vlib/v/gen/c/mmap_headerless_test.v index 603698090c..444b191fee 100644 --- a/vlib/v/gen/c/mmap_headerless_test.v +++ b/vlib/v/gen/c/mmap_headerless_test.v @@ -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', '') + a.cached_header_sources['/cache/os.vh'] = '/vroot/vlib/os/args.v' + assert g.core_native_header_is_redeclared('/cache/os.vh', '') + a.cached_header_sources['/cache/user.vh'] = '/project/os/args.v' + assert !g.core_native_header_is_redeclared('/cache/user.vh', '') + g.set_target(pref.target_from('linux', 'arm32')!) + assert !g.core_native_header_is_redeclared('/cache/os.vh', '') + g.set_target(pref.target_from('macos', 'arm64')!) g.no_builtin = true assert !g.core_native_header_is_redeclared(source, '') g.no_builtin = false diff --git a/vlib/v/gen/c/stmt.v b/vlib/v/gen/c/stmt.v index 646dda5ba1..b3de9b153b 100644 --- a/vlib/v/gen/c/stmt.v +++ b/vlib/v/gen/c/stmt.v @@ -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('}') diff --git a/vlib/v/gen/c/sumtype_semantics_test.v b/vlib/v/gen/c/sumtype_semantics_test.v index 842f8c3683..395941098c 100644 --- a/vlib/v/gen/c/sumtype_semantics_test.v +++ b/vlib/v/gen/c/sumtype_semantics_test.v @@ -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 + } } diff --git a/vlib/v/gen/fastc/declaration_abi_test.v b/vlib/v/gen/fastc/declaration_abi_test.v index fc9d215ed6..7614de15c0 100644 --- a/vlib/v/gen/fastc/declaration_abi_test.v +++ b/vlib/v/gen/fastc/declaration_abi_test.v @@ -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 \n' + source + '\nptrdiff_t native_difference(void) { return -15; }\n') + } +} diff --git a/vlib/v/gen/fastc/stmt.v b/vlib/v/gen/fastc/stmt.v index 47b4f90107..03da9ce4f2 100644 --- a/vlib/v/gen/fastc/stmt.v +++ b/vlib/v/gen/fastc/stmt.v @@ -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) diff --git a/vlib/v/gen/fastc/str_intp.v b/vlib/v/gen/fastc/str_intp.v index 498c3aea7e..9b23fd2f27 100644 --- a/vlib/v/gen/fastc/str_intp.v +++ b/vlib/v/gen/fastc/str_intp.v @@ -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() diff --git a/vlib/v/tests/builtin_strings_and_interpolation/string_interpolation_struct_with_usize_field_test.v b/vlib/v/tests/builtin_strings_and_interpolation/string_interpolation_struct_with_usize_field_test.v index 406a347ad6..d922770ab7 100644 --- a/vlib/v/tests/builtin_strings_and_interpolation/string_interpolation_struct_with_usize_field_test.v +++ b/vlib/v/tests/builtin_strings_and_interpolation/string_interpolation_struct_with_usize_field_test.v @@ -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}' } diff --git a/vlib/v/tests/casts/cast_interface_value_in_match_test.v b/vlib/v/tests/casts/cast_interface_value_in_match_test.v index d50a63b9a7..504b06dcb1 100644 --- a/vlib/v/tests/casts/cast_interface_value_in_match_test.v +++ b/vlib/v/tests/casts/cast_interface_value_in_match_test.v @@ -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)) } diff --git a/vlib/v/tests/translated_scalar_promotions_test.v b/vlib/v/tests/translated_scalar_promotions_test.v index d86f31ea48..9cd0c8beeb 100644 --- a/vlib/v/tests/translated_scalar_promotions_test.v +++ b/vlib/v/tests/translated_scalar_promotions_test.v @@ -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 { diff --git a/vlib/v/types/checker_tail.v b/vlib/v/types/checker_tail.v index 337eaf555f..cace95b02a 100644 --- a/vlib/v/types/checker_tail.v +++ b/vlib/v/types/checker_tail.v @@ -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) } diff --git a/vlib/v/types/checker_tail_stmt.v b/vlib/v/types/checker_tail_stmt.v index f29338b602..a7ff5229ba 100644 --- a/vlib/v/types/checker_tail_stmt.v +++ b/vlib/v/types/checker_tail_stmt.v @@ -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))) { diff --git a/vlib/v/types/lexical_parent_cache_test.v b/vlib/v/types/lexical_parent_cache_test.v index 0ef558d85a..cbbf488830 100644 --- a/vlib/v/types/lexical_parent_cache_test.v +++ b/vlib/v/types/lexical_parent_cache_test.v @@ -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