From fdf1d4b8b182d77187ff4e0c02d98d935c6dec76 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Tue, 23 Jun 2026 00:19:40 +0300 Subject: [PATCH] v3: add test_all self-host coverage (#27539) --- vlib/v3/flat/flat.v | 7 +- vlib/v3/gen/c/fn.v | 11 + vlib/v3/gen/c/interface.v | 32 + vlib/v3/markused/markused.v | 28 +- vlib/v3/parser/parser.v | 64 +- vlib/v3/ssa/builder.v | 11 + vlib/v3/test_all.vsh | 225 ++++++ vlib/v3/tests/generics_test.v | 71 +- vlib/v3/tests/markused_test.v | 280 +++++++ vlib/v3/tests/parallel_cgen_test.v | 8 +- vlib/v3/tests/test_all_lang_features.out | 957 +++++++++++++++++++++++ vlib/v3/tests/test_all_lang_features.v | 16 +- vlib/v3/tests/test_v3.vsh | 121 +-- vlib/v3/tests/type_checker_errors_test.v | 43 +- vlib/v3/types/checker.v | 247 +++++- vlib/v3/v3.v | 63 +- 16 files changed, 2038 insertions(+), 146 deletions(-) create mode 100644 vlib/v3/test_all.vsh create mode 100644 vlib/v3/tests/test_all_lang_features.out diff --git a/vlib/v3/flat/flat.v b/vlib/v3/flat/flat.v index 9e3ea102d..9ca85ad36 100644 --- a/vlib/v3/flat/flat.v +++ b/vlib/v3/flat/flat.v @@ -136,9 +136,10 @@ pub enum Op { pub struct Node { pub mut: - value string - typ string - kind_id int + value string + typ string + generic_params []string + kind_id int pub: pos token.Pos children_start i32 diff --git a/vlib/v3/gen/c/fn.v b/vlib/v3/gen/c/fn.v index c7f744ffd..d741cda89 100644 --- a/vlib/v3/gen/c/fn.v +++ b/vlib/v3/gen/c/fn.v @@ -46,7 +46,14 @@ fn (mut g FlatGen) should_emit_fn_node_in_module(node flat.Node, node_index int, if cfn in ['Array_string__join', 'array_string_join'] { return false } + if g.has_used_fn_filter() && is_generated_fn_after_markused(node.value) { + return !g.has_generic_params(node) + } if module_name == 'main' { + if g.has_used_fn_filter() && !g.used_fn_contains(node.value) && !g.used_fn_contains(dfn) + && !g.used_fn_contains(cfn) && !g.used_fn_contains(qfn) { + return false + } return !g.has_generic_params(node) } if g.has_used_fn_filter() && !g.used_fn_contains(node.value) && !g.used_fn_contains(dfn) @@ -59,6 +66,10 @@ fn (mut g FlatGen) should_emit_fn_node_in_module(node flat.Node, node_index int, return true } +fn is_generated_fn_after_markused(name string) bool { + return name.starts_with('__anon_fn_') +} + fn (g &FlatGen) used_fn_contains(name string) bool { if name.len == 0 { return false diff --git a/vlib/v3/gen/c/interface.v b/vlib/v3/gen/c/interface.v index 9bfa7e272..9ef4e3c54 100644 --- a/vlib/v3/gen/c/interface.v +++ b/vlib/v3/gen/c/interface.v @@ -262,6 +262,9 @@ fn (mut g FlatGen) interface_method_stubs() { for iface_name, methods in g.interfaces { cn := c_name(iface_name) for method in methods { + if !g.should_emit_interface_dispatch(iface_name, method) { + continue + } g.gen_interface_dispatch(iface_name, cn, method) } } @@ -270,6 +273,35 @@ fn (mut g FlatGen) interface_method_stubs() { } } +fn (g &FlatGen) should_emit_interface_dispatch(iface_name string, method string) bool { + if !g.has_used_fn_filter() { + return true + } + name := '${iface_name}.${method}' + if g.used_fn_contains(name) || g.used_fn_contains(c_name(name)) { + return true + } + short_name := '${iface_name.all_after_last('.')}.${method}' + return short_name != name && g.interface_dispatch_short_name_is_unambiguous(short_name, method) + && (g.used_fn_contains(short_name) || g.used_fn_contains(c_name(short_name))) +} + +fn (g &FlatGen) interface_dispatch_short_name_is_unambiguous(short_name string, method string) bool { + mut matches := 0 + for iface_name, methods in g.interfaces { + if method !in methods { + continue + } + if '${iface_name.all_after_last('.')}.${method}' == short_name { + matches++ + if matches > 1 { + return false + } + } + } + return matches == 1 +} + fn (mut g FlatGen) gen_interface_dispatch(iface_name string, cn string, method string) { sid := g.intern_string('interface method ${cn}.${method} not implemented') mname := '${iface_name}.${method}' diff --git a/vlib/v3/markused/markused.v b/vlib/v3/markused/markused.v index e4685ba74..ef56a93fa 100644 --- a/vlib/v3/markused/markused.v +++ b/vlib/v3/markused/markused.v @@ -414,7 +414,7 @@ fn enqueue_auto_roots(fn_decls map[string]FnDeclInfo, mut used map[string]bool, fn enqueue_main_module_roots(fn_decls map[string]FnDeclInfo, mut used map[string]bool, mut queue []string) { for name, info in fn_decls { - if info.module != 'main' || name.contains('.') { + if info.module != 'main' || name != 'main' { continue } enqueue(name, mut used, mut queue) @@ -470,6 +470,9 @@ fn enqueue_detected_runtime_helpers(a &flat.FlatAst, tc &types.TypeChecker, mut && (fn_node.value == 'error' || fn_node.value == 'error_with_code') { needs_optional_helpers = true } + if fn_node.kind == .ident && fn_node.value == 'flag_default_value' { + enqueue('escape_default_string', mut used, mut queue) + } if fn_node.kind == .ident && fn_node.value in ['print', 'println', 'eprint', 'eprintln'] && node.children_count >= 2 { @@ -784,7 +787,10 @@ fn (c &CallCollector) collect_calls(node &flat.Node, cur_module string, imports } } base_type := c.node_type(base_id) - type_name := resolve_type_name(base_type) + mut type_name := resolve_type_name(base_type) + if type_name.len == 0 && base.kind == .ident { + type_name = c.value_type_name(base.value, cur_module) + } if type_name.len > 0 { calls << type_name + '.' + callee.value } @@ -1155,6 +1161,24 @@ fn (c &CallCollector) node_type(id flat.NodeId) types.Type { return c.tc.resolve_type(id) } +fn (c &CallCollector) value_type_name(name string, cur_module string) string { + for candidate in [qualify_fn(cur_module, name), name] { + if typ := c.tc.file_scope.lookup(candidate) { + type_name := resolve_type_name(typ) + if type_name.len > 0 { + return type_name + } + } + if typ := c.tc.const_types[candidate] { + type_name := resolve_type_name(typ) + if type_name.len > 0 { + return type_name + } + } + } + return '' +} + fn (c &CallCollector) collect_zero_struct_default_calls(typ types.Type, cur_module string, imports map[string]string, mut calls []string) { type_name := zero_value_struct_type_name(typ) if type_name.len == 0 { diff --git a/vlib/v3/parser/parser.v b/vlib/v3/parser/parser.v index a6a833869..4a2ef614b 100644 --- a/vlib/v3/parser/parser.v +++ b/vlib/v3/parser/parser.v @@ -1026,8 +1026,9 @@ fn (mut p Parser) fn_decl_body(name string, receiver_name string, receiver_type disable_body := p.disable_fn_body p.disable_fn_body = false // generic params — skip + mut generic_params := []string{} if p.tok == .lsbr { - p.skip_brackets() + generic_params = p.parse_generic_param_names() } // params @@ -1065,6 +1066,7 @@ fn (mut p Parser) fn_decl_body(name string, receiver_name string, receiver_type kind: .c_fn_decl value: name typ: ret_type + generic_params: generic_params children_start: start children_count: flat.child_count(param_ids.len) }) @@ -1103,6 +1105,7 @@ fn (mut p Parser) fn_decl_body(name string, receiver_name string, receiver_type kind: .fn_decl value: name typ: ret_type + generic_params: generic_params children_start: start children_count: flat.child_count(all_ids.len) }) @@ -1184,9 +1187,10 @@ fn (mut p Parser) struct_decl() flat.NodeId { } // generic params — skip mut is_generic := false + mut generic_params := []string{} if p.tok == .lsbr { is_generic = true - p.skip_brackets() + generic_params = p.parse_generic_param_names() } // implements clause if p.tok == .name && p.lit == 'implements' { @@ -1203,9 +1207,10 @@ fn (mut p Parser) struct_decl() flat.NodeId { p.next() } return p.a.add_node(flat.Node{ - kind: .struct_decl - value: name - typ: struct_decl_typ(is_union, is_generic) + kind: .struct_decl + value: name + typ: struct_decl_typ(is_union, is_generic) + generic_params: generic_params }) } p.check(.lcbr) @@ -1338,6 +1343,7 @@ fn (mut p Parser) struct_decl() flat.NodeId { kind: .struct_decl value: name typ: struct_decl_typ(is_union, is_generic) + generic_params: generic_params children_start: start children_count: flat.child_count(ids.len) }) @@ -1595,8 +1601,9 @@ fn (mut p Parser) type_decl() flat.NodeId { } name := p.expect_name() // generic params + mut generic_params := []string{} if p.tok == .lsbr { - p.skip_brackets() + generic_params = p.parse_generic_param_names() } p.expect(.assign) first_type := p.parse_type_name() @@ -1620,6 +1627,7 @@ fn (mut p Parser) type_decl() flat.NodeId { return p.a.add_node(flat.Node{ kind: .type_decl value: name + generic_params: generic_params children_start: start children_count: flat.child_count(variants.len) }) @@ -1629,9 +1637,10 @@ fn (mut p Parser) type_decl() flat.NodeId { } // type alias return p.a.add_node(flat.Node{ - kind: .type_decl - value: name - typ: first_type + kind: .type_decl + value: name + typ: first_type + generic_params: generic_params }) } @@ -1643,8 +1652,9 @@ fn (mut p Parser) interface_decl() flat.NodeId { name += '.' + p.expect(.name) } // generic params + mut generic_params := []string{} if p.tok == .lsbr { - p.skip_brackets() + generic_params = p.parse_generic_param_names() } p.check(.lcbr) mut ids := []flat.NodeId{} @@ -1730,6 +1740,7 @@ fn (mut p Parser) interface_decl() flat.NodeId { return p.a.add_node(flat.Node{ kind: .interface_decl value: name + generic_params: generic_params children_start: start children_count: flat.child_count(ids.len) }) @@ -2076,6 +2087,39 @@ fn (mut p Parser) skip_brackets() { } } +fn (mut p Parser) parse_generic_param_names() []string { + mut names := []string{} + if p.tok != .lsbr { + return names + } + mut depth := 1 + mut expect_name := true + p.next() + for depth > 0 && p.tok != .eof { + if p.tok == .lsbr { + depth++ + expect_name = false + } else if p.tok == .rsbr { + depth-- + if depth == 0 { + p.next() + break + } + } else if depth == 1 { + if p.tok == .comma { + expect_name = true + } else if expect_name && p.tok == .name { + names << p.lit + expect_name = false + } else { + expect_name = false + } + } + p.next() + } + return names +} + fn (mut p Parser) skip_parens() { if p.tok != .lpar { return diff --git a/vlib/v3/ssa/builder.v b/vlib/v3/ssa/builder.v index a9766829c..e3086f6e4 100644 --- a/vlib/v3/ssa/builder.v +++ b/vlib/v3/ssa/builder.v @@ -7729,6 +7729,17 @@ fn (mut b Builder) build_call(id flat.NodeId, node flat.Node) ValueID { } } if resolved_name !in b.fn_ids && resolved_name.contains('.') { + qualified_name := ssa_fn_name_in_module(b.cur_module, resolved_name) + if qualified_name in b.fn_ids { + resolved_name = qualified_name + if fn_node.kind == .selector { + if has_receiver := b.fn_signature_has_receiver(resolved_name, + node.children_count - 1) + { + is_method = has_receiver + } + } + } c_name := resolved_name.replace('.', '__') if c_name in b.fn_ids { resolved_name = c_name diff --git a/vlib/v3/test_all.vsh b/vlib/v3/test_all.vsh new file mode 100644 index 000000000..d1919a2ea --- /dev/null +++ b/vlib/v3/test_all.vsh @@ -0,0 +1,225 @@ +#!/usr/bin/env -S v + +import os + +const total_steps = 6 +const temp_prefix = 'v3_test_all' + +struct Config { + vexe string + script_dir string + repo_root string + tests_dir string + v3_src string + host_backend string + host_os string +} + +fn main() { + cfg := parse_config() + os.chdir(cfg.repo_root) or { fail('failed to enter ${cfg.repo_root}: ${err}') } + + v3_bin := temp_path('v3') + hello_c_bin := temp_path('hello_c') + hello_arm_bin := temp_path('hello_arm64') + v4_arm_bin := temp_path('v4_arm64') + v3_lang_bin := temp_path('v3_lang') + v4_bin := temp_path('v4_chain') + v5_bin := temp_path('v5_chain') + v6_bin := temp_path('v6_chain') + cleanup_files([ + v3_bin, + hello_c_bin, + hello_c_bin + '.c', + hello_arm_bin, + v4_arm_bin, + v3_lang_bin, + v3_lang_bin + '.c', + v4_bin, + v4_bin + '.c', + v5_bin, + v5_bin + '.c', + v6_bin, + v6_bin + '.c', + ]) + + section(1, 'V3 unit tests') + run('${q(cfg.vexe)} -silent test ${q(cfg.script_dir)}') + + section(2, 'Build v3') + run('${q(cfg.vexe)} -o ${q(v3_bin)} ${q(cfg.v3_src)}') + + section(3, 'C backend hello world') + hello_v := os.join_path(cfg.tests_dir, 'hello.v') + run('${q(v3_bin)} ${q(hello_v)} -b c -o ${q(hello_c_bin)}') + run(q(hello_c_bin)) + cleanup_files([hello_c_bin, hello_c_bin + '.c']) + + section(4, 'ARM64 self-host hello world') + if cfg.host_backend == 'arm64' && cfg.host_os == 'macos' { + run('${q(v3_bin)} --no-parallel -selfhost -b arm64 -o ${q(v4_arm_bin)} ${q(cfg.v3_src)}') + run('${q(v4_arm_bin)} --no-parallel -b arm64 -o ${q(hello_arm_bin)} ${q(hello_v)}') + run(q(hello_arm_bin)) + cleanup_files([v4_arm_bin, hello_arm_bin]) + } else { + println(' Skipping ARM64 self-host on ${cfg.host_os}/${cfg.host_backend} host (Mach-O only)') + } + + section(5, 'Self-host chain (v3->v4->v5->v6)') + println(' Building v4 from v3...') + run('${q(v3_bin)} --no-parallel -selfhost -o ${q(v4_bin)} ${q(cfg.v3_src)}') + println(' Building v5 from v4...') + run('${q(v4_bin)} --no-parallel -selfhost -o ${q(v5_bin)} ${q(cfg.v3_src)}') + println(' Building v6 from v5...') + run('${q(v5_bin)} --no-parallel -selfhost -o ${q(v6_bin)} ${q(cfg.v3_src)}') + converged_size := assert_same_file_bytes('v5/v6 generated C output', v5_bin + '.c', v6_bin + + '.c') + println(' v5.c=v6.c (${converged_size} bytes) - chain converged') + cleanup_files([v4_bin, v4_bin + '.c', v5_bin, v5_bin + '.c', v6_bin, v6_bin + '.c']) + + section(6, 'Language feature parity') + lang_v := os.join_path(cfg.tests_dir, 'test_all_lang_features.v') + lang_out := os.join_path(cfg.tests_dir, 'test_all_lang_features.out') + run('${q(v3_bin)} ${q(lang_v)} -b c -o ${q(v3_lang_bin)}') + v3_c_out := run_output(q(v3_lang_bin)) + expected_out := read_text_file(lang_out) + assert_same_text('language feature output', v3_c_out, expected_out) + println(' v3 C OK (${v3_c_out.split_into_lines().len} lines)') + println(' ARM64 coverage is the one-generation macOS self-host smoke test in step 4') + cleanup_files([v3_bin, v3_lang_bin, v3_lang_bin + '.c']) + + println('') + println('=== ALL TESTS PASSED ===') +} + +fn parse_config() Config { + script_dir := os.real_path(@DIR) + repo_root := os.real_path(os.join_path(script_dir, '..', '..')) + tests_dir := os.join_path(script_dir, 'tests') + vexe := absolute_path(@VEXE) + if !os.is_executable(vexe) { + fail('FAIL: V compiler not found: ${vexe}') + } + return Config{ + vexe: vexe + script_dir: script_dir + repo_root: repo_root + tests_dir: tests_dir + v3_src: os.join_path(script_dir, 'v3.v') + host_backend: native_backend_arch() + host_os: os.user_os() + } +} + +fn native_backend_arch() string { + machine := os.uname().machine.to_lower() + match machine { + 'x86_64', 'amd64' { + return 'x64' + } + 'aarch64', 'arm64' { + return 'arm64' + } + else { + return machine + } + } +} + +fn temp_path(name string) string { + return os.join_path(os.temp_dir(), '${temp_prefix}_${name}') +} + +fn absolute_path(path string) string { + if os.is_abs_path(path) { + return path + } + return os.join_path(os.getwd(), path) +} + +fn section(step int, title string) { + if step > 1 { + println('') + } + println('=== ${step}/${total_steps}: ${title} ===') +} + +fn run(cmd string) { + println('> ${cmd}') + code := os.system(cmd) + if code != 0 { + exit(code) + } +} + +fn run_output(cmd string) string { + stdout_path := temp_path('stdout') + cleanup_files([stdout_path]) + println('> ${cmd}') + code := os.system('${cmd} > ${q(stdout_path)}') + if code != 0 { + exit(code) + } + content := read_text_file(stdout_path) + cleanup_files([stdout_path]) + return content +} + +fn read_text_file(path string) string { + content := os.read_file(path) or { + fail('FAIL: failed to read ${path}: ${err}') + return '' + } + return content +} + +fn read_binary_file(path string) []u8 { + content := os.read_bytes(path) or { + fail('FAIL: failed to read ${path}: ${err}') + return []u8{} + } + return content +} + +fn assert_same_file_bytes(label string, left_path string, right_path string) int { + left := read_binary_file(left_path) + right := read_binary_file(right_path) + if left != right { + fail('FAIL: ${label} differs byte-for-byte (${left.len} bytes vs ${right.len} bytes)') + } + return left.len +} + +fn assert_same_text(label string, actual string, expected string) { + if actual == expected { + return + } + actual_lines := actual.split_into_lines() + expected_lines := expected.split_into_lines() + min_lines := if actual_lines.len < expected_lines.len { + actual_lines.len + } else { + expected_lines.len + } + for i in 0 .. min_lines { + if actual_lines[i] != expected_lines[i] { + fail('FAIL: ${label} differs at line ${i + 1}: expected `${expected_lines[i]}`, got `${actual_lines[i]}`') + } + } + fail('FAIL: ${label} line count differs: expected ${expected_lines.len}, got ${actual_lines.len}') +} + +fn cleanup_files(paths []string) { + for path in paths { + os.rm(path) or {} + } +} + +fn q(path string) string { + return os.quoted_path(path) +} + +fn fail(message string) { + eprintln(message) + exit(1) +} diff --git a/vlib/v3/tests/generics_test.v b/vlib/v3/tests/generics_test.v index a0e5bd800..25b425066 100644 --- a/vlib/v3/tests/generics_test.v +++ b/vlib/v3/tests/generics_test.v @@ -22,6 +22,27 @@ fn run_selfhost_bad(v3_bin string, name string, src string, expected string) { assert !result.output.contains('C compilation failed') } +fn write_project_file(root string, rel string, src string) { + path := os.join_path(root, rel) + os.mkdir_all(os.dir(path)) or { panic(err) } + os.write_file(path, src) or { panic(err) } +} + +fn run_selfhost_project_bad(v3_bin string, name string, files map[string]string, input string, expected string) { + root := os.join_path(os.temp_dir(), 'v3_gen_${name}_project') + os.rmdir_all(root) or {} + os.mkdir_all(root) or { panic(err) } + for rel, src in files { + write_project_file(root, rel, src) + } + input_path := os.join_path(root, input) + bad_bin := os.join_path(os.temp_dir(), 'v3_gen_${name}') + result := os.execute('${v3_bin} ${input_path} -selfhost -b c -o ${bad_bin}') + assert result.exit_code != 0, 'expected error for ${name}, but compilation succeeded' + assert result.output.contains(expected), 'expected "${expected}" in output for ${name}, got: ${result.output}' + assert !result.output.contains('C compilation failed') +} + fn run_no_generic_error(v3_bin string, name string, src string) { src_file := os.join_path(os.temp_dir(), 'v3_gen_${name}.v') os.write_file(src_file, src) or { panic(err) } @@ -70,9 +91,42 @@ fn main() { b := Box[int]{value: 7} println(b.value) } -', + ', 'unsupported generic') + // generic struct with no generic fields + run_selfhost_bad(v3_bin, 'generic_struct_marker_only', ' +struct Phantom[T] { + value int +} +fn main() {} +', + 'unsupported generic struct `Phantom`') + + run_selfhost_bad(v3_bin, 'generic_fn_marker_only', ' +fn unused[T]() {} +fn main() {} + ', + 'unsupported generic declaration `unused`') + + run_selfhost_bad(v3_bin, 'generic_c_fn_marker_only', ' +fn helper[T]() +fn main() {} + ', + 'unsupported generic declaration `helper`') + + run_selfhost_bad(v3_bin, 'generic_interface_marker_only', ' +interface I[T] {} +fn main() {} + ', + 'unsupported generic declaration `I`') + + run_selfhost_bad(v3_bin, 'generic_type_alias_marker_only', ' +type Alias[T] = int +fn main() {} +', + 'unsupported generic declaration `Alias`') + // generic method run_selfhost_bad(v3_bin, 'generic_method', ' struct Box[T] { @@ -151,6 +205,21 @@ interface Container[T] { fn main() {} ', 'unsupported generic') + + run_selfhost_project_bad(v3_bin, 'imported_generic_struct', { + 'main.v': 'module main + +import badmod + +fn main() {} +' + 'badmod/badmod.v': 'module badmod + +struct Phantom[T] { + value int +} +' + }, 'main.v', 'unsupported generic struct `Phantom`') } fn test_generics_allowed_without_building_v() { diff --git a/vlib/v3/tests/markused_test.v b/vlib/v3/tests/markused_test.v index 08bbeb9ba..91467ea63 100644 --- a/vlib/v3/tests/markused_test.v +++ b/vlib/v3/tests/markused_test.v @@ -27,6 +27,42 @@ fn parse_checked_source(name string, source string) (&flat.FlatAst, &types.TypeC return a, &tc } +fn parse_checked_project(name string, files map[string]string, main_file string) (&flat.FlatAst, &types.TypeChecker) { + root := os.join_path(os.temp_dir(), 'v3_markused_${name}') + os.rmdir_all(root) or {} + for rel, source in files { + path := os.join_path(root, rel) + os.mkdir_all(os.dir(path)) or { panic(err) } + os.write_file(path, source) or { panic(err) } + } + mut paths := []string{} + if main_file.len > 0 { + paths << os.join_path(root, main_file) + } + mut rel_paths := []string{} + for rel, _ in files { + if rel != main_file { + rel_paths << rel + } + } + rel_paths.sort() + for rel in rel_paths { + paths << os.join_path(root, rel) + } + prefs := pref.new_preferences() + mut p := parser.Parser.new(prefs) + mut a := p.parse_files(paths) + mut tc := types.TypeChecker.new(a) + tc.collect(a) + tc.diagnose_unknown_calls = true + for path in paths { + tc.diagnostic_files[path] = true + } + tc.check_semantics() + assert tc.errors.len == 0, tc.errors.str() + return a, &tc +} + fn mark_used_source(name string, source string) map[string]bool { a, tc := parse_checked_source(name, source) return markused.mark_used(a, tc) @@ -116,6 +152,250 @@ fn main() { assert used['string__plus'] } +fn test_receiver_method_call_in_selector_assign_rhs_is_used() { + used := mark_used_source('receiver_method_selector_assign_rhs', ' +struct Builder { +mut: + name string +} + +fn (b &Builder) main_module_name() string { + return "main" +} + +fn build_with_options() string { + mut b := Builder{} + b.name = b.main_module_name() + return b.name +} + +fn main() { + _ := build_with_options() +} +') + assert used['Builder.main_module_name'] +} + +fn test_unreachable_interface_implementer_method_is_not_rooted() { + used := mark_used_source('unreachable_interface_implementer', ' +interface Reader { + read() int +} + +struct File {} + +fn (f File) read() int { + return 1 +} + +fn main() {} +') + assert !used['File.read'] +} + +fn test_reachable_interface_dispatch_keeps_implementer_method() { + used := mark_used_source('reachable_interface_dispatch', ' +interface Reader { + read() int +} + +struct File {} + +fn (f File) read() int { + return 1 +} + +fn call_reader(r Reader) int { + return r.read() +} + +fn main() { + _ := call_reader(File{}) +} +') + assert used['File.read'] +} + +fn test_global_interface_dispatch_keeps_implementer_method() { + used := mark_used_source('global_interface_dispatch', ' +interface Reader { + read() int +} + +struct File {} + +__global default_reader &Reader + +fn (f File) read() int { + return 1 +} + +fn call_default_reader() int { + return default_reader.read() +} + +fn main() { + _ := call_default_reader() +} +') + assert used['Reader.read'] + assert used['File.read'] +} + +fn test_unreachable_interface_dispatch_stub_is_not_emitted_after_used_filter_transform() { + mut a, mut tc := parse_checked_source('unreachable_interface_dispatch_cgen', ' +interface Reader { + read() int +} + +struct File {} + +fn (f File) read() int { + return 1 +} + +fn main() {} +') + used := markused.mark_used(a, tc) + assert !used['Reader.read'] + assert !used['File.read'] + transform.transform_with_used(mut a, tc, used) + tc.diagnose_unknown_calls = false + tc.reject_unlowered_map_mutation = true + tc.annotate_types() + mut g := cgen.FlatGen.new() + c_code := g.gen_with_used_options(a, used, tc, true) + assert !c_code.contains('Reader__read(') +} + +fn test_short_interface_dispatch_does_not_emit_imported_name_collision_stub() { + mut a, mut tc := parse_checked_project('interface_dispatch_name_collision', { + 'main.v': 'module main + +import moda + +interface Reader { + read() int +} + +struct Local {} + +fn (l Local) read() int { + return 1 +} + +fn call_reader(r Reader) int { + return r.read() +} + +fn main() { + _ := call_reader(Local{}) +} +' + 'moda/moda.v': 'module moda + +interface Reader { + read(path string) string +} + +struct Remote {} + +fn (r Remote) read(path string) string { + return path +} +' + }, 'main.v') + used := markused.mark_used(a, tc) + assert used['Reader.read'] + assert !used['moda.Reader.read'] + transform.transform_with_used(mut a, tc, used) + tc.diagnose_unknown_calls = false + tc.reject_unlowered_map_mutation = true + tc.annotate_types() + mut g := cgen.FlatGen.new() + c_code := g.gen_with_used_options(a, used, tc, true) + assert c_code.contains('Reader__read(') + assert !c_code.contains('moda__Reader__read(') +} + +fn test_unused_main_method_with_interface_dispatch_is_pruned_with_stub() { + mut a, mut tc := parse_checked_source('unused_main_method_interface_dispatch_cgen', ' +interface Reader { + read() int +} + +struct File {} + +fn (f File) read() int { + return 1 +} + +struct X {} + +fn (x X) unused(r Reader) int { + return r.read() +} + +fn main() {} +') + used := markused.mark_used(a, tc) + assert !used['X.unused'] + assert !used['Reader.read'] + transform.transform_with_used(mut a, tc, used) + tc.diagnose_unknown_calls = false + tc.reject_unlowered_map_mutation = true + tc.annotate_types() + mut g := cgen.FlatGen.new() + c_code := g.gen_with_used_options(a, used, tc, true) + assert !c_code.contains('X__unused(') + assert !c_code.contains('Reader__read(') +} + +fn test_unused_main_helper_with_method_call_is_pruned_with_method() { + mut a, mut tc := parse_checked_source('unused_main_helper_method_call_cgen', + 'module main\n\nstruct X {}\n\nfn (x X) m() int {\n\treturn 1\n}\n\nfn helper() int {\n\treturn X{}.m()\n}\n\nfn main() {}\n') + used := markused.mark_used(a, tc) + assert !used['helper'] + assert !used['X.m'] + transform.transform_with_used(mut a, tc, used) + tc.diagnose_unknown_calls = false + tc.reject_unlowered_map_mutation = true + tc.annotate_types() + mut g := cgen.FlatGen.new() + c_code := g.gen_with_used_options(a, used, tc, true) + assert !c_code.contains('helper(') + assert !c_code.contains('X__m(') +} + +fn test_reachable_main_fn_literal_is_emitted_after_used_filter_transform() { + mut a, mut tc := parse_checked_source('reachable_main_fn_literal_cgen', + 'module main\n\nfn callback_value(cb fn () int) int {\n\treturn cb()\n}\n\nfn main() {\n\t_ := callback_value(fn () int {\n\t\treturn 7\n\t})\n}\n') + used := markused.mark_used(a, tc) + assert used['callback_value'] + transform.transform_with_used(mut a, tc, used) + tc.diagnose_unknown_calls = false + tc.reject_unlowered_map_mutation = true + tc.annotate_types() + mut g := cgen.FlatGen.new() + c_code := g.gen_with_used_options(a, used, tc, true) + assert c_code.contains('int __anon_fn_') + assert c_code.contains('callback_value(__anon_fn_') +} + +fn test_flag_default_value_lowering_keeps_escape_helper() { + mut a, mut tc := parse_checked_source('flag_default_value_escape_helper_cgen', + 'module main\n\nfn escape_default_string(value string) string {\n\treturn value\n}\n\nfn flag_default_value(value string) string {\n\treturn value\n}\n\nfn main() {\n\t_ := flag_default_value("abc")\n}\n') + used := markused.mark_used(a, tc) + assert used['escape_default_string'] + transform.transform_with_used(mut a, tc, used) + tc.diagnose_unknown_calls = false + tc.reject_unlowered_map_mutation = true + tc.annotate_types() + mut g := cgen.FlatGen.new() + c_code := g.gen_with_used_options(a, used, tc, true) + assert c_code.contains('escape_default_string(') +} + fn test_return_local_address_seeds_memdup_runtime_helper() { used := mark_used_source('return_local_address_memdup', ' struct Box { diff --git a/vlib/v3/tests/parallel_cgen_test.v b/vlib/v3/tests/parallel_cgen_test.v index c75287778..a514e136d 100644 --- a/vlib/v3/tests/parallel_cgen_test.v +++ b/vlib/v3/tests/parallel_cgen_test.v @@ -21,13 +21,17 @@ fn write_parallel_module_init_project(name string) string { main_src.writeln('import moda') main_src.writeln('') for i in 0 .. 1050 { - main_src.writeln('fn unused_${i}() int {') + main_src.writeln('fn helper_${i}() int {') main_src.writeln('\treturn ${i}') main_src.writeln('}') main_src.writeln('') } main_src.writeln('fn main() {') - main_src.writeln('\tprintln(int_str(moda.value()))') + main_src.writeln('\tmut total := 0') + for i in 0 .. 1050 { + main_src.writeln('\ttotal += helper_${i}()') + } + main_src.writeln('\tprintln(int_str(total + moda.value()))') main_src.writeln('}') os.write_file(os.join_path(project_dir, 'main.v'), main_src.str()) or { panic(err) } diff --git a/vlib/v3/tests/test_all_lang_features.out b/vlib/v3/tests/test_all_lang_features.out new file mode 100644 index 000000000..334cd2ce8 --- /dev/null +++ b/vlib/v3/tests/test_all_lang_features.out @@ -0,0 +1,957 @@ +=== v3 Test Suite === +--- 1. Struct Declaration & Initialization --- +10 +20 +0 +0 +100 +200 +14 +21 +4 +6 +--- 2. Calls & Selector Assignment --- +15 +35 +12 +15 +20 +25 +50 +--- 3. Globals & Compound Assignment --- +100 +70 +30 +25 +50 +84 +--- 4. Bool & Logic --- +1 +0 +1 +1 +1 +1 +--- 5. Loop with Break/Continue --- +23 +25 +15 +37 +10 +--- 6. Match --- +777 +200 +111 +500 +40 +--- 7. C-style Loop --- +120 +55 +256 +55 +30 +--- 8. Recursive Functions --- +55 +720 +55 +6 +1024 +--- 9. Nested Loops --- +9 +20 +36 +6 +15 +--- 10. Infinite Loop --- +5 +21 +128 +0 +10 +--- 11. Many Arguments --- +8 +36 +16 +9 +36 +--- 12. Modifying Struct via Function --- +999 +888 +15 +5 +30 +60 +105 +210 +0 +0 +--- 13. Assert --- +Assert 1 passed +Assert 2 passed +Assert 3 passed +Assert 4 passed +Assert 5 passed +--- 14. Heap Allocation --- +10 +20 +0 +0 +25 +36 +100 +200 +42 +1 +2 +--- 15. Bitwise Operations --- +8 +14 +6 +205 +5 +4 +--- 16. Shift Operations --- +16 +8 +15 +56 +6 +--- 17. Modulo --- +2 +2 +1 +0 +4 +5 +4 +--- 18. Pointer Arithmetic --- +10 +20 +400 +600 +50 +100 +90 +300 +--- 19. Nested Struct Access --- +100 +200 +110 +100 +200 +40 +60 +120 +--- 20. Negative Numbers --- +-42 +-5 +-30 +-21 +50 +--- 21. Else-If Chains --- +3 +2 +1 +0 +3 +--- 22. Function Returning Struct --- +10 +20 +15 +24 +40 +60 +4 +6 +300 +--- 23. Early Return --- +42 +17 +0 +10 +20 +--- 24. Clamp & Multi-Arg Functions --- +10 +100 +50 +10 +100 +--- 25. Postfix Inc/Dec --- +11 +9 +5 +101 +10 +--- 26. Compound Bitwise Assignment --- +15 +10 +6 +16 +16 +--- 27. Complex Boolean --- +1 +1 +1 +1 +1 +--- 28. Iterative Algorithms --- +8 +111 +0 +15 +45 +--- 29. Bit Counting --- +0 +3 +8 +1 +4 +--- 30. Global Counter Patterns --- +45 +1 +15 +55 +120 +42 +--- 31. Nested Struct Mutation --- +50 +60 +50 +100 +15 +25 +1200 +20 +60 +--- 32. Quadrant & Struct Passing --- +1 +2 +3 +4 +0 +--- 33. 4-Field Struct --- +255 +128 +100 +100 +0 +50 +765 +128 +--- 34. Fibonacci Iterative --- +55 +6765 +88 +12 +987 +--- 35. Nested Loops with Flow Control --- +30 +10 +27 +36 +12 +--- 36. Complex Match --- +13 +400 +400 +2111 +300 +--- 37. Chained Function Calls --- +10 +42 +15 +15 +25 +--- 38. Mixed Arithmetic --- +257 +31 +20 +20 +249 +--- 39. Large Computations --- +3628800 +1048576 +2870 +5050 +40320 +--- 40. Integration Test --- +15 +30 +21 +26 +5 +Integration asserts passed +55 +100 +--- 41. Vector Math --- +32 +14 +5 +7 +9 +--- 42. Vector Scale & Cross --- +3 +6 +9 +-3 +14 +--- 43. Matrix Operations --- +-2 +5 +19 +22 +50 +--- 44. Prime Checking --- +10 +1 +0 +1 +0 +--- 45. Integer Square Root --- +0 +1 +2 +10 +9 +--- 46. Reverse & Palindrome --- +54321 +1 +5 +1 +0 +--- 47. Stats Tracking --- +3 +25 +60 +5 +12 +--- 48. Binary Search --- +2 +0 +4 +-1 +3 +--- 49. Ackermann Function --- +1 +3 +7 +29 +6 +--- 50. Triangle & Geometry --- +12 +100 +-4 +3 +7 +--- 51. Digital Root --- +0 +5 +3 +9 +6 +--- 52. Interpolation & Sign --- +50 +13 +1 +-1 +0 +--- 53. Bit Manipulation --- +0 +3 +8 +30 +32 +--- 54. Chained Struct Operations --- +8 +16 +10 +18 +10 +--- 55. Global Accumulation --- +15 +30 +45 +90 +3150 +--- 56. Sieve Simulation --- +328 +47 +2 +6 +--- 57. Complex Loop Patterns --- +55 +55 +10 +55 +89 +--- 58. Heap Struct Computations --- +1400 +625 +4 +40 +150 +--- 59. Multi-Function Pipeline --- +2 +2 +195 +55 +55 +--- 60. Stress Integration --- +190 +0 +30 +25 +All stress assertions passed +=== ALL 60 TESTS PASSED === +--- 61. Methods --- +30 +30 +14 +16 +50 +30 +130 +9 +20 +--- 62. If-Expressions --- +5 +7 +0 +20 +30 +5 +10 +15 +8 +-1 +1 +0 +5 +0 +10 +50 +30 +--- 63. String Interpolation --- +The answer is 42 +10 + 20 = 30 +Value: 100! +999 +1-2-3 +Hello World +ABC +--- 64. For-In Range --- +10 +35 +14 +42 +120 +10 +25 +--- 65. Enums --- +0 +3 +10 +1 +300 +--- 66. Defer Statements --- +42 +100 +111 +--- 67. Unary Operations --- +1 +0 +1 +1 +1 +--- 68. Complex Boolean --- +1 +1 +1 +1 +1 +--- 69. Comparison as Expression --- +1 +1 +1 +1 +1 +--- 70. Deeply Nested If --- +1 +20 +300 +3 +8 +--- 71. Large Constants --- +100000 +1000000 +100000 +100000 +10000 +--- 72. Mixed Operations --- +1 +1 +11 +11 +4 +--- 73. Edge Cases --- +0 +0 +0 +42 +42 +42 +1 +10 +0 +5 +1 +1 +--- 74. Complex Recursion --- +5050 +21 +610 +243 +5040 +--- 75. Struct Operations --- +0 +0 +15 +25 +300 +100 +40 +20 +1 +--- 76. Control Flow Edge Cases --- +1 +7 +22 +55 +100 +200 +300 +15 +--- 77. Array Initialization --- +10 +20 +30 +30 +7 +14 +21 +400 +7 +20 +--- 78. For-In Array --- +60 +70 +6 +9 +180 +--- 79. Fixed Size Arrays --- +5 +10 +15 +10 +30 +120 +103 +1 +5 +5 +15 +--- 80. String Struct Fields --- +Hello +5 +5 +Passed directly +--- 81. Struct Field Operations --- +15 +17 +42 +25 +75 +20 +40 +8 +7 +28 +15 +18 +2 +2 +8 +13 +15 +30 +20 +25 +100 +50 +--- 82. Println --- +hello world +--- 83. Algebraic Optimizations --- +0 +0 +99 +77 +50 +0 +10 +26 +5 +14 +0 +0 +12345 +12345 +10 +--- 84. Dead Store Elimination --- +1 +30 +5 +100 +75 +--- 85. Goto Statement --- +3 +1 +--- 86. String Match Return --- +__plus +__minus +__mul +? +--- 87. Return If Expression --- +return if expr: ok +--- 88. Or Blocks and Optional --- +42 +99 +42 +7 +or blocks: ok +--- 89. If Guard (Optional Unwrap) --- +42 +99 +84 +50 +1 +if guard: ok +--- 90. Maps --- +1 +2 +3 +3 +2 +99 +100 +200 +10 +20 +maps: ok +--- 91. String Methods --- +1 +2 +3 +4 +5 +6 +4 +7 +hello +world +hello +world +/Users/test +file.v +hello +abc +abc +42 +string methods: ok +--- 92. Dynamic Arrays --- +3 +10 +20 +30 +60 +20 +2 +hello +world +dynamic arrays: ok +--- 93. Array Methods, Slicing, Split --- +3 +4 +1 +3 +2 +20 +2 +200 +0 +contains 10: yes +contains 7: no +hello +world +hello +3 +20 +30 +3 +a +b +c +hello v3 +1 +array methods, slicing, split: ok +--- 94. Map Iteration, Array Init with len --- +6 +6 +5 +0 +3 +42 +42 +map iteration, array init: ok +--- 95. In Operator, Array Join --- +hello in map: yes +missing not in map: yes +20 in array: yes +99 not in array: yes +hello, world, v3 +hello world v3 +in operator, array join: ok +hello world +line1 +line2 +line3 +strings.Builder: ok +static methods: ok +@FILE: ok +unsafe blocks: ok +function pointers: ok +sum type smartcast: ok +--- 102. In Operator LHS Evaluation --- +range side effect: yes +1 +inline side effect: no +1 +not in side effect: yes +1 +in operator lhs eval: ok +--- 103. Transformer Semantic Lowering --- +interp 7 true +6 +297 +12 +42 +9 +42 +4 +map membership lowered: yes +10 +Milo +transformer semantic lowering: ok +--- 104. Review Regression Lowering --- +2 +v3 +2 +100 +Bolt +8 +7 +-1 +7 +-1 +42 +review regression lowering: ok +--- 105. Self-Hosting Features --- +10 +11 +2 +1 +hello +world +3 +2 +Socks and Buddy +self-hosting features: ok +--- 106. Multi-Return Functions --- +42 +hello +20 +10 +30 +33 +3 +multi-return: ok +--- 107. Struct Update Syntax (Assoc) --- +99 +20 +10 +55 +3 +42 +3 +struct update (assoc): ok +--- 108. Array Push Many --- +5 +2 +9 +6 +20 +third +array push_many: ok +--- 109. String Comparison Operators --- +1 +1 +1 +1 +cat +string comparison: ok +--- 110. Struct Operator Overloading --- +4 +6 +1 +1 +14 +18 +struct operators: ok +--- 111. Return Match / Match as Expression --- +one +three +B +zero +one-two +match expression: ok +--- 112. If Expression Returning String --- +yes +big +positive +Result: pass +odd +if expr string: ok +--- 113. @FN Comptime --- +main +@FN and @FILE: ok +--- 114. String in Expressions --- +1 +less +1 +Hello, World +3 +string expressions: ok +--- 115. Self-Host Regression Features --- +14 +9 +3 +resolved +7 +resolved +11 +7 +self-host regression features: ok +--- 116. Additional Self-Host Feature Coverage --- +v3 +7 +v3 +42 +2 +11 +int literal +1 +2 +45 +additional self-host feature coverage: ok +--- 117. ARM64 Self-Host Regression Coverage --- +map[string][]Type +Type +fn(ArrayDataHeader) base_data +Writer +Reader +8 +1 +-1 +3 +SSA +2 +20 +2 +5 +scoped +1 +12 +24 +9 +23 +31 +as-cast +Elem +Array +4 +-5 +0 +1 +116 +8 +[]Type +1 +1 +27 +1 +15 +38 +80 +33 +92 +41 +18 +6 +1 +35 +12 +7 +match-id +not-ident +idx +1 +0 +fallback +6 +125 +27 +38 +11 +83 +71 +75 +101 +21 +29 +31 +Option(none)|Option('halt') +Option('tail') +Option('set') +"abc" +41 +79 +43 +9 +4 +13 +38 +47 +53 +61 +16 +67 +89 +97 +59 +103 +107 +109 +113 +94 +117 +arm64 self-host regression coverage: ok +=== ALL 119 TESTS PASSED === diff --git a/vlib/v3/tests/test_all_lang_features.v b/vlib/v3/tests/test_all_lang_features.v index 232d858e2..071f1839b 100644 --- a/vlib/v3/tests/test_all_lang_features.v +++ b/vlib/v3/tests/test_all_lang_features.v @@ -1581,7 +1581,7 @@ fn C.atomic_compare_exchange_strong_u16(voidptr, voidptr, u16) bool fn C.atomic_compare_exchange_weak_u32(voidptr, voidptr, u32) bool -fn C.atomic_compare_exchange_weak_byte(voidptr, voidptr, byte) bool +fn C.atomic_compare_exchange_weak_byte(voidptr, voidptr, u8) bool fn C.atomic_compare_exchange_weak_u64(voidptr, voidptr, u64) bool @@ -1788,8 +1788,10 @@ fn module_function_new119() int { fn nil_newline_pointer_assignment119() int { mut item119 := NilLink119{} mut link119 := &item119.next - item119.next = nil - *link119 = &item119 + unsafe { + item119.next = nil + *link119 = &item119 + } if isnil(item119.next) { return 0 } @@ -1868,7 +1870,7 @@ fn map_index_or_none119(items map[string]MapMethodValue119, name string) ?MapMet return items[name] or { none } } -fn map_index_bang119(items map[string]MapMethodValue119, name string) ?MapMethodValue119 { +fn map_index_bang119(items map[string]MapMethodValue119, name string) !MapMethodValue119 { return items[name]! } @@ -1971,9 +1973,9 @@ fn c_atomic_channel_helpers119() int { if C.atomic_compare_exchange_weak_u32(voidptr(&counter119), voidptr(&expected32_119), u32(5)) { score119 += int(counter119) } - mut locked119 := byte(0) - mut expected_byte119 := byte(0) - if C.atomic_compare_exchange_weak_byte(voidptr(&locked119), voidptr(&expected_byte119), byte(8)) { + mut locked119 := u8(0) + mut expected_byte119 := u8(0) + if C.atomic_compare_exchange_weak_byte(voidptr(&locked119), voidptr(&expected_byte119), u8(8)) { score119 += int(locked119) } mut big119 := u64(10) diff --git a/vlib/v3/tests/test_v3.vsh b/vlib/v3/tests/test_v3.vsh index b2b3e0137..2d40c3362 100644 --- a/vlib/v3/tests/test_v3.vsh +++ b/vlib/v3/tests/test_v3.vsh @@ -3,10 +3,10 @@ import os const vexe = @VEXE -const v1exe = 'v' const tests_dir = os.dir(@FILE) const v3_dir = os.dir(tests_dir) const test_v = os.join_path(tests_dir, 'test_all_lang_features.v') +const test_out = os.join_path(tests_dir, 'test_all_lang_features.out') const v3_src = os.join_path(v3_dir, 'v3.v') fn run(cmd string) os.Result { @@ -26,22 +26,6 @@ fn build_v3() string { return v3_bin } -fn run_v1() string { - v1_bin := '${os.temp_dir()}/v1_test' - r := run('${v1exe} -enable-globals -o ${v1_bin} ${test_v}') - if r.exit_code != 0 { - eprintln('FAIL: global v compilation failed') - eprintln(r.output) - exit(1) - } - r2 := run(v1_bin) - if r2.exit_code != 0 { - eprintln('FAIL: global v binary crashed (exit ${r2.exit_code})') - exit(1) - } - return r2.output -} - fn run_v3_c(v3_bin string) string { v3c_bin := '${os.temp_dir()}/v3c_test' r := run('${v3_bin} ${test_v} -b c -o ${v3c_bin}') @@ -50,79 +34,60 @@ fn run_v3_c(v3_bin string) string { eprintln(r.output) exit(1) } - r2 := run(v3c_bin) - if r2.exit_code != 0 { - eprintln('FAIL: v3 C binary crashed (exit ${r2.exit_code})') - exit(1) + return run_stdout(v3c_bin) +} + +fn run_stdout(cmd string) string { + stdout_path := '${os.temp_dir()}/v3c_test_stdout' + os.rm(stdout_path) or {} + println('> ${cmd}') + code := os.system('${cmd} > ${os.quoted_path(stdout_path)}') + if code != 0 { + eprintln('FAIL: command failed (exit ${code})') + exit(code) } - return r2.output + output := read_text_file(stdout_path) + os.rm(stdout_path) or {} + return output } -fn run_v3_arm64(v3_bin string) string { - v3arm_bin := '${os.temp_dir()}/v3arm_test' - r := run('${v3_bin} ${test_v} -b arm64 -o ${v3arm_bin}') - if r.exit_code != 0 { - eprintln('FAIL: v3 arm64 backend compilation failed') - eprintln(r.output) +fn read_text_file(path string) string { + content := os.read_file(path) or { + eprintln('FAIL: failed to read ${path}: ${err}') exit(1) } - r2 := run(v3arm_bin) - if r2.exit_code != 0 { - eprintln('FAIL: v3 arm64 binary crashed (exit ${r2.exit_code})') - exit(1) + return content +} + +fn assert_same_text(label string, actual string, expected string) { + if actual == expected { + return + } + actual_lines := actual.split_into_lines() + expected_lines := expected.split_into_lines() + min_lines := if actual_lines.len < expected_lines.len { + actual_lines.len + } else { + expected_lines.len } - return r2.output + for i in 0 .. min_lines { + if actual_lines[i] != expected_lines[i] { + eprintln('FAIL: ${label} differs at line ${i + 1}: expected `${expected_lines[i]}`, got `${actual_lines[i]}`') + exit(1) + } + } + eprintln('FAIL: ${label} line count differs: expected ${expected_lines.len}, got ${actual_lines.len}') + exit(1) } // Build v3 v3_bin := build_v3() println('built v3: ${v3_bin}') -// Run all three -v1_out := run_v1() -println('v1: OK') - +// Run V3 backends v3c_out := run_v3_c(v3_bin) +expected_out := read_text_file(test_out) +assert_same_text('v3 C fixture output', v3c_out, expected_out) println('v3 C: OK') -v3arm_out := run_v3_arm64(v3_bin) -println('v3 arm64: OK') - -// Compare -mut ok := true - -if v3c_out != v1_out { - eprintln('FAIL: v3 C output differs from v1') - v1_lines := v1_out.split_into_lines() - v3c_lines := v3c_out.split_into_lines() - for i in 0 .. v1_lines.len { - if i < v3c_lines.len && v1_lines[i] != v3c_lines[i] { - eprintln(' line ${i + 1}: v1="${v1_lines[i]}" v3c="${v3c_lines[i]}"') - } - } - if v1_lines.len != v3c_lines.len { - eprintln(' v1: ${v1_lines.len} lines, v3c: ${v3c_lines.len} lines') - } - ok = false -} - -if v3arm_out != v1_out { - eprintln('FAIL: v3 arm64 output differs from v1') - v1_lines := v1_out.split_into_lines() - v3arm_lines := v3arm_out.split_into_lines() - for i in 0 .. v1_lines.len { - if i < v3arm_lines.len && v1_lines[i] != v3arm_lines[i] { - eprintln(' line ${i + 1}: v1="${v1_lines[i]}" arm64="${v3arm_lines[i]}"') - } - } - if v1_lines.len != v3arm_lines.len { - eprintln(' v1: ${v1_lines.len} lines, arm64: ${v3arm_lines.len} lines') - } - ok = false -} - -if ok { - println('=== ALL BACKENDS MATCH (${v1_out.split_into_lines().len} lines) ===') -} else { - exit(1) -} +println('=== V3 C OK (${v3c_out.split_into_lines().len} lines) ===') diff --git a/vlib/v3/tests/type_checker_errors_test.v b/vlib/v3/tests/type_checker_errors_test.v index aaa782b23..4ee0d410a 100644 --- a/vlib/v3/tests/type_checker_errors_test.v +++ b/vlib/v3/tests/type_checker_errors_test.v @@ -13,10 +13,18 @@ fn build_v3() string { } fn run_bad(v3_bin string, name string, src string, expected string) { + run_bad_with_flags(v3_bin, name, src, expected, '') +} + +fn run_bad_selfhost(v3_bin string, name string, src string, expected string) { + run_bad_with_flags(v3_bin, name, src, expected, '-selfhost') +} + +fn run_bad_with_flags(v3_bin string, name string, src string, expected string, flags string) { bad_src := os.join_path(os.temp_dir(), 'v3_${name}.v') os.write_file(bad_src, src) or { panic(err) } bad_bin := os.join_path(os.temp_dir(), 'v3_${name}') - result := os.execute('${v3_bin} ${bad_src} -b c -o ${bad_bin}') + result := os.execute('${v3_bin} ${bad_src} ${flags} -b c -o ${bad_bin}') assert result.exit_code != 0 assert result.output.contains(expected) assert !result.output.contains('C compilation failed') @@ -27,10 +35,10 @@ fn run_good(v3_bin string, name string, src string) string { os.write_file(good_src, src) or { panic(err) } good_bin := os.join_path(os.temp_dir(), 'v3_${name}') compile := os.execute('${v3_bin} ${good_src} -b c -o ${good_bin}') - assert compile.exit_code == 0 - assert !compile.output.contains('C compilation failed') + assert compile.exit_code == 0, '${name}: compile failed: ${compile.output}' + assert !compile.output.contains('C compilation failed'), '${name}: C compilation failed: ${compile.output}' run := os.execute(good_bin) - assert run.exit_code == 0 + assert run.exit_code == 0, '${name}: run failed: ${run.output}' return run.output.trim_space() } @@ -69,10 +77,10 @@ fn run_good_project(v3_bin string, name string, files map[string]string, input s input_path := if input.len == 0 { root } else { os.join_path(root, input) } good_bin := os.join_path(os.temp_dir(), 'v3_${name}') compile := os.execute('${v3_bin} ${input_path} -b c -o ${good_bin}') - assert compile.exit_code == 0 - assert !compile.output.contains('C compilation failed') + assert compile.exit_code == 0, '${name}: compile failed: ${compile.output}' + assert !compile.output.contains('C compilation failed'), '${name}: C compilation failed: ${compile.output}' run := os.execute(good_bin) - assert run.exit_code == 0 + assert run.exit_code == 0, '${name}: run failed: ${run.output}' return run.output.trim_space() } @@ -145,10 +153,27 @@ fn test_type_checker_reports_core_semantic_errors() { '`Bird` is not a variant of sum type `Animal`') run_bad(v3_bin, 'bad_unknown_decl_type', 'fn f(x Missing) {}\nfn main() {}\n', 'unknown type `Missing`') - run_bad(v3_bin, 'bad_generic_param', + run_bad(v3_bin, 'bad_unknown_generic_application_base', + 'fn f(x Missing[int]) {}\nfn main() {}\n', 'unknown type `Missing`') + run_bad(v3_bin, 'bad_unknown_generic_application_arg', + 'struct Box[T] {\n\tvalue T\n}\nfn f(x Box[Missing]) {}\nfn main() {}\n', + 'unknown type `Missing`') + run_bad(v3_bin, 'bad_single_letter_struct_arg', + 'struct A {}\nfn takes_a(a A) {}\nfn main() {\n\ttakes_a(1)\n}\n', + 'cannot use `int` as argument 1 to `takes_a`; expected `A`') + run_bad(v3_bin, 'bad_unknown_single_letter_type', 'fn f(x Z) {}\nfn main() {}\n', + 'unknown type `Z`') + run_bad(v3_bin, 'bad_repeated_unknown_single_letter_type', + 'fn f(x Z) Z {\n\treturn x\n}\nfn main() {}\n', 'unknown type `Z`') + run_bad(v3_bin, 'bad_undeclared_generic_fn_param', 'fn f[T](x T, y Z) {}\nfn main() {}\n', + 'unknown type `Z`') + run_bad(v3_bin, 'bad_undeclared_generic_struct_field', + 'struct Box[T] {\n\tother U\n}\nfn main() {}\n', 'unknown type `U`') + run_bad_selfhost(v3_bin, 'bad_generic_param', 'fn id[T](x T) T {\n\treturn x\n}\nfn main() {\n\t_ := id(1)\n}\n', 'unsupported generic type parameter `T`') - run_bad(v3_bin, 'bad_generic_type_application', 'fn takes_box(x Box[int]) {}\nfn main() {}\n', + run_bad_selfhost(v3_bin, 'bad_generic_type_application', + 'fn takes_box(x Box[int]) {}\nfn main() {}\n', 'unsupported generic type application `Box[int]`') run_bad_project(v3_bin, 'bad_bare_imported_call', { 'main.v': 'module main\n\nimport moda\n\nfn main() {\n\t_ := answer()\n}\n' diff --git a/vlib/v3/types/checker.v b/vlib/v3/types/checker.v index ae9399d6e..28dc644ef 100644 --- a/vlib/v3/types/checker.v +++ b/vlib/v3/types/checker.v @@ -114,6 +114,7 @@ pub mut: checking_nodes []bool diagnose_unknown_calls bool reject_unlowered_map_mutation bool + reject_unsupported_generics bool diagnostic_files map[string]bool cur_fn_ret_type Type = Type(void_) smartcasts map[string]Type @@ -212,6 +213,17 @@ fn (mut tc TypeChecker) record_error(kind TypeErrorKind, msg string, node flat.N } } +fn (mut tc TypeChecker) record_unsupported_generic(msg string, node flat.NodeId) { + if !tc.should_diagnose_unsupported_generic(node) { + return + } + tc.errors << TypeError{ + msg: msg + kind: .unsupported_generic + node: node + } +} + pub fn (mut tc TypeChecker) collect(a &flat.FlatAst) { tc.a = a tc.file_scope = new_scope(unsafe { nil }) @@ -1027,6 +1039,11 @@ pub fn (mut tc TypeChecker) check_semantics() { tc.pop_scope() tc.cur_fn_ret_type = Type(void_) } + .c_fn_decl { + if tc.reject_unsupported_generics { + tc.check_decl_type_strings(flat.NodeId(i), node) + } + } else {} } @@ -1046,8 +1063,17 @@ fn (mut tc TypeChecker) check_fn_body(node flat.Node) { } fn (mut tc TypeChecker) check_decl_type_strings(node_id flat.NodeId, node flat.Node) { - if !(node.kind == .struct_decl && node.typ.contains('union')) { - tc.check_type_string_for_unsupported_generics(node.typ, node_id) + generic_params := tc.infer_decl_generic_params(node) + if node.kind == .struct_decl { + if node.typ.contains('generic') && tc.reject_unsupported_generics { + tc.record_unsupported_generic('unsupported generic struct `${node.value}`', node_id) + } + } else { + if node.generic_params.len > 0 && tc.reject_unsupported_generics { + tc.record_unsupported_generic('unsupported generic declaration `${node.value}`', + node_id) + } + tc.check_type_string_for_unsupported_generics(node.typ, node_id, generic_params) } for i in 0 .. node.children_count { child_id := tc.a.child(&node, i) @@ -1055,9 +1081,9 @@ fn (mut tc TypeChecker) check_decl_type_strings(node_id flat.NodeId, node flat.N continue } child := tc.a.nodes[int(child_id)] - tc.check_type_string_for_unsupported_generics(child.typ, child_id) + tc.check_type_string_for_unsupported_generics(child.typ, child_id, generic_params) if node.kind == .type_decl && child.value.len > 0 { - tc.check_type_string_for_unsupported_generics(child.value, child_id) + tc.check_type_string_for_unsupported_generics(child.value, child_id, generic_params) } for j in 0 .. child.children_count { grandchild_id := tc.a.child(&child, j) @@ -1065,73 +1091,227 @@ fn (mut tc TypeChecker) check_decl_type_strings(node_id flat.NodeId, node flat.N continue } grandchild := tc.a.nodes[int(grandchild_id)] - tc.check_type_string_for_unsupported_generics(grandchild.typ, grandchild_id) + tc.check_type_string_for_unsupported_generics(grandchild.typ, grandchild_id, + generic_params) } } } -fn (mut tc TypeChecker) check_type_string_for_unsupported_generics(typ string, node_id flat.NodeId) { +fn (tc &TypeChecker) infer_decl_generic_params(node flat.Node) map[string]bool { + mut params := map[string]bool{} + for name in node.generic_params { + params[name] = true + } + tc.collect_generic_receiver_params(node, mut params) + return params +} + +fn (tc &TypeChecker) collect_generic_receiver_params(node flat.Node, mut params map[string]bool) { + if node.kind != .fn_decl && node.kind != .c_fn_decl { + return + } + if !node.value.contains('.') { + return + } + if node.children_count == 0 { + return + } + receiver_id := tc.a.child(&node, 0) + if int(receiver_id) < 0 { + return + } + receiver := tc.a.nodes[int(receiver_id)] + if receiver.kind != .param { + return + } + receiver_type := receiver.typ.trim_left('&') + if receiver_type != node.value.all_before_last('.') { + return + } + mut counts := map[string]int{} + if !tc.collect_generic_param_candidates(receiver.typ, mut counts) { + return + } + for name, _ in counts { + params[name] = true + } +} + +fn (tc &TypeChecker) collect_generic_param_candidates(typ string, mut counts map[string]int) bool { + clean := typ.trim_space() + if clean.len == 0 { + return false + } + if clean.starts_with('&') || clean.starts_with('?') || clean.starts_with('!') { + return tc.collect_generic_param_candidates(clean[1..], mut counts) + } + if clean.starts_with('shared ') { + return tc.collect_generic_param_candidates(clean[7..], mut counts) + } + if clean.starts_with('...') { + return tc.collect_generic_param_candidates(clean[3..], mut counts) + } + if clean.starts_with('[]') { + return tc.collect_generic_param_candidates(clean[2..], mut counts) + } + if clean.starts_with('map[') { + mut found_context := false + bracket_end := find_matching_bracket(clean, 3) + if bracket_end < clean.len { + if tc.collect_generic_param_candidates(clean[4..bracket_end], mut counts) { + found_context = true + } + if tc.collect_generic_param_candidates(clean[bracket_end + 1..], mut counts) { + found_context = true + } + } + return found_context + } + if clean.starts_with('[') { + idx := clean.index_u8(`]`) + if idx > 0 { + return tc.collect_generic_param_candidates(clean[idx + 1..], mut counts) + } + return false + } + if clean.starts_with('(') && clean.ends_with(')') { + mut found_context := false + for part in split_params(clean[1..clean.len - 1]) { + if tc.collect_generic_param_candidates(part, mut counts) { + found_context = true + } + } + return found_context + } + if clean.starts_with('fn(') || clean.starts_with('fn (') { + mut found_context := false + params_start := clean.index_u8(`(`) + 1 + mut depth := 1 + mut params_end := params_start + for params_end < clean.len { + if clean[params_end] == `(` { + depth++ + } else if clean[params_end] == `)` { + depth-- + if depth == 0 { + break + } + } + params_end++ + } + if params_end < clean.len { + for part in split_params(clean[params_start..params_end]) { + trimmed := part.trim_space() + parts := trimmed.split(' ') + param_type := if parts.len >= 2 { parts[parts.len - 1] } else { trimmed } + if tc.collect_generic_param_candidates(param_type, mut counts) { + found_context = true + } + } + if tc.collect_generic_param_candidates(clean[params_end + 1..], mut counts) { + found_context = true + } + } + return found_context + } + if generic_type_application(clean) { + bracket := clean.index_u8(`[`) + bracket_end := find_matching_bracket(clean, bracket) + if bracket_end < clean.len { + for part in split_params(clean[bracket + 1..bracket_end]) { + tc.collect_generic_param_candidates(part, mut counts) + } + } + return true + } + if is_bare_generic_param(clean) && !tc.type_name_known(clean) { + counts[clean] = (counts[clean] or { 0 }) + 1 + } + return false +} + +fn (mut tc TypeChecker) check_type_string_for_unsupported_generics(typ string, node_id flat.NodeId, generic_params map[string]bool) { clean := typ.trim_space() if clean.len == 0 { return } if clean.starts_with('&') || clean.starts_with('?') || clean.starts_with('!') { - tc.check_type_string_for_unsupported_generics(clean[1..], node_id) + tc.check_type_string_for_unsupported_generics(clean[1..], node_id, generic_params) return } if clean.starts_with('shared ') { - tc.check_type_string_for_unsupported_generics(clean[7..], node_id) + tc.check_type_string_for_unsupported_generics(clean[7..], node_id, generic_params) return } if clean.starts_with('...') { - tc.check_type_string_for_unsupported_generics(clean[3..], node_id) + tc.check_type_string_for_unsupported_generics(clean[3..], node_id, generic_params) return } if clean.starts_with('[]') { - tc.check_type_string_for_unsupported_generics(clean[2..], node_id) + tc.check_type_string_for_unsupported_generics(clean[2..], node_id, generic_params) return } if clean.starts_with('map[') { bracket_end := find_matching_bracket(clean, 3) if bracket_end < clean.len { - tc.check_type_string_for_unsupported_generics(clean[4..bracket_end], node_id) - tc.check_type_string_for_unsupported_generics(clean[bracket_end + 1..], node_id) + tc.check_type_string_for_unsupported_generics(clean[4..bracket_end], node_id, + generic_params) + tc.check_type_string_for_unsupported_generics(clean[bracket_end + 1..], node_id, + generic_params) } return } if clean.starts_with('[') { idx := clean.index_u8(`]`) if idx > 0 { - tc.check_type_string_for_unsupported_generics(clean[idx + 1..], node_id) + tc.check_type_string_for_unsupported_generics(clean[idx + 1..], node_id, generic_params) } return } if clean.starts_with('(') && clean.ends_with(')') { for part in split_params(clean[1..clean.len - 1]) { - tc.check_type_string_for_unsupported_generics(part, node_id) + tc.check_type_string_for_unsupported_generics(part, node_id, generic_params) } return } if clean.starts_with('fn(') || clean.starts_with('fn (') { - tc.check_fn_type_string_for_unsupported_generics(clean, node_id) + tc.check_fn_type_string_for_unsupported_generics(clean, node_id, generic_params) return } if generic_type_application(clean) { - tc.record_error(.unsupported_generic, 'unsupported generic type application `${clean}`', - node_id) + if tc.reject_unsupported_generics { + tc.record_unsupported_generic('unsupported generic type application `${clean}`', + node_id) + return + } + bracket := clean.index_u8(`[`) + bracket_end := find_matching_bracket(clean, bracket) + base := clean[..bracket].trim_space() + if should_check_named_type(base) && !tc.type_name_known(base) { + tc.record_error(.unknown_type, 'unknown type `${base}`', node_id) + } + if bracket_end < clean.len { + for part in split_params(clean[bracket + 1..bracket_end]) { + tc.check_type_string_for_unsupported_generics(part, node_id, generic_params) + } + } return } if is_bare_generic_param(clean) && !tc.type_name_known(clean) { - tc.record_error(.unsupported_generic, 'unsupported generic type parameter `${clean}`', - node_id) - return + if tc.reject_unsupported_generics { + tc.record_unsupported_generic('unsupported generic type parameter `${clean}`', node_id) + return + } + if clean in generic_params { + return + } } if should_check_named_type(clean) && !tc.type_name_known(clean) { tc.record_error(.unknown_type, 'unknown type `${clean}`', node_id) } } -fn (mut tc TypeChecker) check_fn_type_string_for_unsupported_generics(typ string, node_id flat.NodeId) { +fn (mut tc TypeChecker) check_fn_type_string_for_unsupported_generics(typ string, node_id flat.NodeId, generic_params map[string]bool) { params_start := typ.index_u8(`(`) + 1 mut depth := 1 mut params_end := params_start @@ -1153,10 +1333,10 @@ fn (mut tc TypeChecker) check_fn_type_string_for_unsupported_generics(typ string trimmed := part.trim_space() parts := trimmed.split(' ') param_type := if parts.len >= 2 { parts[parts.len - 1] } else { trimmed } - tc.check_type_string_for_unsupported_generics(param_type, node_id) + tc.check_type_string_for_unsupported_generics(param_type, node_id, generic_params) } ret := typ[params_end + 1..].trim_space() - tc.check_type_string_for_unsupported_generics(ret, node_id) + tc.check_type_string_for_unsupported_generics(ret, node_id, generic_params) } fn generic_type_application(typ string) bool { @@ -1879,6 +2059,19 @@ fn (tc &TypeChecker) should_diagnose(id flat.NodeId) bool { return tc.cur_file in tc.diagnostic_files } +fn (tc &TypeChecker) should_diagnose_unsupported_generic(id flat.NodeId) bool { + if tc.should_diagnose(id) { + return true + } + if int(id) < 0 || int(id) < tc.a.user_code_start { + return false + } + if tc.diagnostic_files.len == 0 { + return false + } + return tc.diagnostic_files['generic:' + tc.cur_file] +} + fn (tc &TypeChecker) should_diagnose_unknown_call(id flat.NodeId) bool { return tc.diagnose_unknown_calls && tc.should_diagnose(id) } @@ -4181,9 +4374,6 @@ fn (tc &TypeChecker) parse_type_uncached(typ string) Type { if typ.len == 0 { return Type(void_) } - if is_generic_placeholder_type(typ) { - return Type(int_) - } if typ.starts_with('&') { return Type(Pointer{ base_type: tc.parse_type(typ[1..]) @@ -4450,6 +4640,9 @@ fn (tc &TypeChecker) parse_type_uncached(typ string) Type { }) } } + if is_generic_placeholder_type(typ) { + return unknown_type('generic type parameter `${typ}`') + } if typ.contains('[') && !typ.starts_with('[') { bracket := typ.index_u8(`[`) bracket_end := typ.index_u8(`]`) @@ -4528,7 +4721,7 @@ fn is_generic_placeholder_type(typ string) bool { last := typ.all_after_last('.') return is_generic_placeholder_type(last) } - return typ in ['T', 'U', 'V', 'K'] + return is_bare_generic_param(typ) } fn (tc &TypeChecker) parse_fn_type(typ string) Type { diff --git a/vlib/v3/v3.v b/vlib/v3/v3.v index 9b2df93e8..ae57b34e5 100644 --- a/vlib/v3/v3.v +++ b/vlib/v3/v3.v @@ -39,6 +39,7 @@ fn main() { mut backend := 'c' mut is_prod := false mut is_strict := false + mut is_selfhost := false mut no_parallel := false mut i := 0 for i < args.len { @@ -52,6 +53,7 @@ fn main() { is_prod = true i++ } else if args[i] == '-selfhost' { + is_selfhost = true i++ } else if args[i] == '-strict' { is_strict = true @@ -114,6 +116,11 @@ fn main() { // Resolve imports recursively resolve_imports(mut a, mut p, prefs, user_files) + diagnostic_root := if is_selfhost { + diagnostic_root_for_input(input_file, user_files) + } else { + '' + } b.step('parse') @@ -121,11 +128,11 @@ fn main() { // (like v2: check runs before transform). The transformer reads cached // per-expression types for type-dependent lowering. mut pre_tc := types.TypeChecker.new(a) + pre_tc.reject_unsupported_generics = is_selfhost pre_tc.collect(a) pre_tc.diagnose_unknown_calls = true - for uf in user_files { - pre_tc.diagnostic_files[uf] = true - } + set_diagnostic_files(mut pre_tc, user_files) + set_unsupported_generic_files(mut pre_tc, a, is_selfhost, diagnostic_root) pre_tc.check_semantics() if pre_tc.errors.len > 0 { print_type_errors(pre_tc.errors) @@ -146,10 +153,9 @@ fn main() { // declarations, and v1/v2 do not run a second semantic checker after lowering. pre_tc.diagnose_unknown_calls = false pre_tc.reject_unlowered_map_mutation = true + set_diagnostic_files(mut pre_tc, user_files) + set_unsupported_generic_files(mut pre_tc, a, is_selfhost, diagnostic_root) pre_tc.annotate_types() - for uf in user_files { - pre_tc.diagnostic_files[uf] = true - } b.step('annotate types') if backend == 'arm64' { @@ -275,6 +281,42 @@ fn print_type_errors(errors []types.TypeError) { } } +fn diagnostic_root_for_input(input_file string, user_files []string) string { + if input_file.len > 0 && os.is_dir(input_file) { + return os.real_path(input_file) + } + if user_files.len > 0 { + return os.real_path(os.dir(user_files[0])) + } + return os.real_path(os.getwd()) +} + +fn set_diagnostic_files(mut tc types.TypeChecker, user_files []string) { + for uf in user_files { + tc.diagnostic_files[uf] = true + } +} + +fn set_unsupported_generic_files(mut tc types.TypeChecker, a &flat.FlatAst, include_imports bool, diagnostic_root string) { + if !include_imports { + return + } + for i, node in a.nodes { + if i < a.user_code_start || node.kind != .file || node.value.len == 0 { + continue + } + if path_is_in_dir(node.value, diagnostic_root) { + tc.diagnostic_files['generic:' + node.value] = true + } + } +} + +fn path_is_in_dir(path string, dir string) bool { + real_path := os.real_path(path) + real_dir := os.real_path(dir) + return real_path == real_dir || real_path.starts_with(real_dir + os.path_separator) +} + fn resolve_imports(mut a flat.FlatAst, mut p parser.Parser, prefs &pref.Preferences, initial_files []string) { mut parsed_modules := map[string]bool{} parsed_modules['builtin'] = true @@ -284,6 +326,7 @@ fn resolve_imports(mut a flat.FlatAst, mut p parser.Parser, prefs &pref.Preferen if initial_files.len > 0 { first_file = initial_files[0] } + project_root := if first_file.len > 0 { os.dir(first_file) } else { os.getwd() } mut changed := true for changed { @@ -307,7 +350,13 @@ fn resolve_imports(mut a flat.FlatAst, mut p parser.Parser, prefs &pref.Preferen changed = true importing_file := if cur_file.len > 0 { cur_file } else { first_file } - mod_dir := prefs.get_module_path(mod_name, importing_file) + mut mod_dir := prefs.get_module_path(mod_name, importing_file) + if mod_dir == '' { + root_mod_dir := os.join_path(project_root, mod_name.replace('.', os.path_separator)) + if os.is_dir(root_mod_dir) { + mod_dir = root_mod_dir + } + } if mod_dir == '' || !os.is_dir(mod_dir) { continue } -- 2.39.5