Skip to content

Commit fc2f06c

Browse files
committed
v3: add test_all self-host coverage
1 parent a66aa5b commit fc2f06c

12 files changed

Lines changed: 1663 additions & 112 deletions

vlib/v3/gen/c/interface.v

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,9 @@ fn (mut g FlatGen) interface_method_stubs() {
262262
for iface_name, methods in g.interfaces {
263263
cn := c_name(iface_name)
264264
for method in methods {
265+
if !g.should_emit_interface_dispatch(iface_name, method) {
266+
continue
267+
}
265268
g.gen_interface_dispatch(iface_name, cn, method)
266269
}
267270
}
@@ -270,6 +273,35 @@ fn (mut g FlatGen) interface_method_stubs() {
270273
}
271274
}
272275

276+
fn (g &FlatGen) should_emit_interface_dispatch(iface_name string, method string) bool {
277+
if !g.has_used_fn_filter() {
278+
return true
279+
}
280+
name := '${iface_name}.${method}'
281+
if g.used_fn_contains(name) || g.used_fn_contains(c_name(name)) {
282+
return true
283+
}
284+
short_name := '${iface_name.all_after_last('.')}.${method}'
285+
return short_name != name && g.interface_dispatch_short_name_is_unambiguous(short_name, method)
286+
&& (g.used_fn_contains(short_name) || g.used_fn_contains(c_name(short_name)))
287+
}
288+
289+
fn (g &FlatGen) interface_dispatch_short_name_is_unambiguous(short_name string, method string) bool {
290+
mut matches := 0
291+
for iface_name, methods in g.interfaces {
292+
if method !in methods {
293+
continue
294+
}
295+
if '${iface_name.all_after_last('.')}.${method}' == short_name {
296+
matches++
297+
if matches > 1 {
298+
return false
299+
}
300+
}
301+
}
302+
return matches == 1
303+
}
304+
273305
fn (mut g FlatGen) gen_interface_dispatch(iface_name string, cn string, method string) {
274306
sid := g.intern_string('interface method ${cn}.${method} not implemented')
275307
mname := '${iface_name}.${method}'

vlib/v3/markused/markused.v

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -784,7 +784,10 @@ fn (c &CallCollector) collect_calls(node &flat.Node, cur_module string, imports
784784
}
785785
}
786786
base_type := c.node_type(base_id)
787-
type_name := resolve_type_name(base_type)
787+
mut type_name := resolve_type_name(base_type)
788+
if type_name.len == 0 && base.kind == .ident {
789+
type_name = c.value_type_name(base.value, cur_module)
790+
}
788791
if type_name.len > 0 {
789792
calls << type_name + '.' + callee.value
790793
}
@@ -1155,6 +1158,24 @@ fn (c &CallCollector) node_type(id flat.NodeId) types.Type {
11551158
return c.tc.resolve_type(id)
11561159
}
11571160

1161+
fn (c &CallCollector) value_type_name(name string, cur_module string) string {
1162+
for candidate in [qualify_fn(cur_module, name), name] {
1163+
if typ := c.tc.file_scope.lookup(candidate) {
1164+
type_name := resolve_type_name(typ)
1165+
if type_name.len > 0 {
1166+
return type_name
1167+
}
1168+
}
1169+
if typ := c.tc.const_types[candidate] {
1170+
type_name := resolve_type_name(typ)
1171+
if type_name.len > 0 {
1172+
return type_name
1173+
}
1174+
}
1175+
}
1176+
return ''
1177+
}
1178+
11581179
fn (c &CallCollector) collect_zero_struct_default_calls(typ types.Type, cur_module string, imports map[string]string, mut calls []string) {
11591180
type_name := zero_value_struct_type_name(typ)
11601181
if type_name.len == 0 {

vlib/v3/ssa/builder.v

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7729,6 +7729,17 @@ fn (mut b Builder) build_call(id flat.NodeId, node flat.Node) ValueID {
77297729
}
77307730
}
77317731
if resolved_name !in b.fn_ids && resolved_name.contains('.') {
7732+
qualified_name := ssa_fn_name_in_module(b.cur_module, resolved_name)
7733+
if qualified_name in b.fn_ids {
7734+
resolved_name = qualified_name
7735+
if fn_node.kind == .selector {
7736+
if has_receiver := b.fn_signature_has_receiver(resolved_name,
7737+
node.children_count - 1)
7738+
{
7739+
is_method = has_receiver
7740+
}
7741+
}
7742+
}
77327743
c_name := resolved_name.replace('.', '__')
77337744
if c_name in b.fn_ids {
77347745
resolved_name = c_name

vlib/v3/test_all.vsh

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
#!/usr/bin/env -S v
2+
3+
import os
4+
5+
const total_steps = 6
6+
const temp_prefix = 'v3_test_all'
7+
8+
struct Config {
9+
vexe string
10+
script_dir string
11+
repo_root string
12+
tests_dir string
13+
v3_src string
14+
host_backend string
15+
}
16+
17+
fn main() {
18+
cfg := parse_config()
19+
os.chdir(cfg.repo_root) or { fail('failed to enter ${cfg.repo_root}: ${err}') }
20+
21+
v3_bin := temp_path('v3')
22+
hello_c_bin := temp_path('hello_c')
23+
hello_arm_bin := temp_path('hello_arm64')
24+
v4_arm_bin := temp_path('v4_arm64')
25+
v3_lang_bin := temp_path('v3_lang')
26+
v4_bin := temp_path('v4_chain')
27+
v5_bin := temp_path('v5_chain')
28+
v6_bin := temp_path('v6_chain')
29+
cleanup_files([
30+
v3_bin,
31+
hello_c_bin,
32+
hello_c_bin + '.c',
33+
hello_arm_bin,
34+
v4_arm_bin,
35+
v3_lang_bin,
36+
v3_lang_bin + '.c',
37+
v4_bin,
38+
v4_bin + '.c',
39+
v5_bin,
40+
v5_bin + '.c',
41+
v6_bin,
42+
v6_bin + '.c',
43+
])
44+
45+
section(1, 'V3 unit tests')
46+
run('${q(cfg.vexe)} -silent test ${q(cfg.script_dir)}')
47+
48+
section(2, 'Build v3')
49+
run('${q(cfg.vexe)} -o ${q(v3_bin)} ${q(cfg.v3_src)}')
50+
51+
section(3, 'C backend hello world')
52+
hello_v := os.join_path(cfg.tests_dir, 'hello.v')
53+
run('${q(v3_bin)} ${q(hello_v)} -b c -o ${q(hello_c_bin)}')
54+
run(q(hello_c_bin))
55+
cleanup_files([hello_c_bin, hello_c_bin + '.c'])
56+
57+
section(4, 'ARM64 self-host hello world')
58+
if cfg.host_backend == 'arm64' {
59+
run('${q(v3_bin)} --no-parallel -selfhost -b arm64 -o ${q(v4_arm_bin)} ${q(cfg.v3_src)}')
60+
run('${q(v4_arm_bin)} --no-parallel -b arm64 -o ${q(hello_arm_bin)} ${q(hello_v)}')
61+
run(q(hello_arm_bin))
62+
cleanup_files([v4_arm_bin, hello_arm_bin])
63+
} else {
64+
println(' Skipping ARM64 self-host on ${cfg.host_backend} host')
65+
}
66+
67+
section(5, 'Self-host chain (v3->v4->v5->v6)')
68+
println(' Building v4 from v3...')
69+
run('${q(v3_bin)} --no-parallel -selfhost -o ${q(v4_bin)} ${q(cfg.v3_src)}')
70+
println(' Building v5 from v4...')
71+
run('${q(v4_bin)} --no-parallel -selfhost -o ${q(v5_bin)} ${q(cfg.v3_src)}')
72+
println(' Building v6 from v5...')
73+
run('${q(v5_bin)} --no-parallel -selfhost -o ${q(v6_bin)} ${q(cfg.v3_src)}')
74+
converged_size := assert_same_file_bytes('v5/v6 generated C output', v5_bin + '.c', v6_bin +
75+
'.c')
76+
println(' v5.c=v6.c (${converged_size} bytes) - chain converged')
77+
cleanup_files([v4_bin, v4_bin + '.c', v5_bin, v5_bin + '.c', v6_bin, v6_bin + '.c'])
78+
79+
section(6, 'Language feature parity')
80+
lang_v := os.join_path(cfg.tests_dir, 'test_all_lang_features.v')
81+
lang_out := os.join_path(cfg.tests_dir, 'test_all_lang_features.out')
82+
run('${q(v3_bin)} ${q(lang_v)} -b c -o ${q(v3_lang_bin)}')
83+
v3_c_out := run_output(q(v3_lang_bin))
84+
expected_out := read_text_file(lang_out)
85+
assert_same_text('language feature output', v3_c_out, expected_out)
86+
println(' v3 C OK (${v3_c_out.split_into_lines().len} lines)')
87+
println(' ARM64 coverage is the one-generation self-host smoke test in step 4')
88+
cleanup_files([v3_bin, v3_lang_bin, v3_lang_bin + '.c'])
89+
90+
println('')
91+
println('=== ALL TESTS PASSED ===')
92+
}
93+
94+
fn parse_config() Config {
95+
script_dir := os.real_path(@DIR)
96+
repo_root := os.real_path(os.join_path(script_dir, '..', '..'))
97+
tests_dir := os.join_path(script_dir, 'tests')
98+
vexe := absolute_path(@VEXE)
99+
if !os.is_executable(vexe) {
100+
fail('FAIL: V compiler not found: ${vexe}')
101+
}
102+
return Config{
103+
vexe: vexe
104+
script_dir: script_dir
105+
repo_root: repo_root
106+
tests_dir: tests_dir
107+
v3_src: os.join_path(script_dir, 'v3.v')
108+
host_backend: native_backend_arch()
109+
}
110+
}
111+
112+
fn native_backend_arch() string {
113+
machine := os.uname().machine.to_lower()
114+
match machine {
115+
'x86_64', 'amd64' {
116+
return 'x64'
117+
}
118+
'aarch64', 'arm64' {
119+
return 'arm64'
120+
}
121+
else {
122+
return machine
123+
}
124+
}
125+
}
126+
127+
fn temp_path(name string) string {
128+
return os.join_path(os.temp_dir(), '${temp_prefix}_${name}')
129+
}
130+
131+
fn absolute_path(path string) string {
132+
if os.is_abs_path(path) {
133+
return path
134+
}
135+
return os.join_path(os.getwd(), path)
136+
}
137+
138+
fn section(step int, title string) {
139+
if step > 1 {
140+
println('')
141+
}
142+
println('=== ${step}/${total_steps}: ${title} ===')
143+
}
144+
145+
fn run(cmd string) {
146+
println('> ${cmd}')
147+
code := os.system(cmd)
148+
if code != 0 {
149+
exit(code)
150+
}
151+
}
152+
153+
fn run_output(cmd string) string {
154+
stdout_path := temp_path('stdout')
155+
cleanup_files([stdout_path])
156+
println('> ${cmd}')
157+
code := os.system('${cmd} > ${q(stdout_path)}')
158+
if code != 0 {
159+
exit(code)
160+
}
161+
content := read_text_file(stdout_path)
162+
cleanup_files([stdout_path])
163+
return content
164+
}
165+
166+
fn read_text_file(path string) string {
167+
content := os.read_file(path) or {
168+
fail('FAIL: failed to read ${path}: ${err}')
169+
return ''
170+
}
171+
return content
172+
}
173+
174+
fn read_binary_file(path string) []u8 {
175+
content := os.read_bytes(path) or {
176+
fail('FAIL: failed to read ${path}: ${err}')
177+
return []u8{}
178+
}
179+
return content
180+
}
181+
182+
fn assert_same_file_bytes(label string, left_path string, right_path string) int {
183+
left := read_binary_file(left_path)
184+
right := read_binary_file(right_path)
185+
if left != right {
186+
fail('FAIL: ${label} differs byte-for-byte (${left.len} bytes vs ${right.len} bytes)')
187+
}
188+
return left.len
189+
}
190+
191+
fn assert_same_text(label string, actual string, expected string) {
192+
if actual == expected {
193+
return
194+
}
195+
actual_lines := actual.split_into_lines()
196+
expected_lines := expected.split_into_lines()
197+
min_lines := if actual_lines.len < expected_lines.len {
198+
actual_lines.len
199+
} else {
200+
expected_lines.len
201+
}
202+
for i in 0 .. min_lines {
203+
if actual_lines[i] != expected_lines[i] {
204+
fail('FAIL: ${label} differs at line ${i + 1}: expected `${expected_lines[i]}`, got `${actual_lines[i]}`')
205+
}
206+
}
207+
fail('FAIL: ${label} line count differs: expected ${expected_lines.len}, got ${actual_lines.len}')
208+
}
209+
210+
fn cleanup_files(paths []string) {
211+
for path in paths {
212+
os.rm(path) or {}
213+
}
214+
}
215+
216+
fn q(path string) string {
217+
return os.quoted_path(path)
218+
}
219+
220+
fn fail(message string) {
221+
eprintln(message)
222+
exit(1)
223+
}

vlib/v3/tests/generics_test.v

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,27 @@ fn run_selfhost_bad(v3_bin string, name string, src string, expected string) {
2222
assert !result.output.contains('C compilation failed')
2323
}
2424

25+
fn write_project_file(root string, rel string, src string) {
26+
path := os.join_path(root, rel)
27+
os.mkdir_all(os.dir(path)) or { panic(err) }
28+
os.write_file(path, src) or { panic(err) }
29+
}
30+
31+
fn run_selfhost_project_bad(v3_bin string, name string, files map[string]string, input string, expected string) {
32+
root := os.join_path(os.temp_dir(), 'v3_gen_${name}_project')
33+
os.rmdir_all(root) or {}
34+
os.mkdir_all(root) or { panic(err) }
35+
for rel, src in files {
36+
write_project_file(root, rel, src)
37+
}
38+
input_path := os.join_path(root, input)
39+
bad_bin := os.join_path(os.temp_dir(), 'v3_gen_${name}')
40+
result := os.execute('${v3_bin} ${input_path} -selfhost -b c -o ${bad_bin}')
41+
assert result.exit_code != 0, 'expected error for ${name}, but compilation succeeded'
42+
assert result.output.contains(expected), 'expected "${expected}" in output for ${name}, got: ${result.output}'
43+
assert !result.output.contains('C compilation failed')
44+
}
45+
2546
fn run_no_generic_error(v3_bin string, name string, src string) {
2647
src_file := os.join_path(os.temp_dir(), 'v3_gen_${name}.v')
2748
os.write_file(src_file, src) or { panic(err) }
@@ -70,9 +91,18 @@ fn main() {
7091
b := Box[int]{value: 7}
7192
println(b.value)
7293
}
73-
',
94+
',
7495
'unsupported generic')
7596

97+
// generic struct with no generic fields
98+
run_selfhost_bad(v3_bin, 'generic_struct_marker_only', '
99+
struct Phantom[T] {
100+
value int
101+
}
102+
fn main() {}
103+
',
104+
'unsupported generic struct `Phantom`')
105+
76106
// generic method
77107
run_selfhost_bad(v3_bin, 'generic_method', '
78108
struct Box[T] {
@@ -151,6 +181,21 @@ interface Container[T] {
151181
fn main() {}
152182
',
153183
'unsupported generic')
184+
185+
run_selfhost_project_bad(v3_bin, 'imported_generic_struct', {
186+
'main.v': 'module main
187+
188+
import badmod
189+
190+
fn main() {}
191+
'
192+
'badmod/badmod.v': 'module badmod
193+
194+
struct Phantom[T] {
195+
value int
196+
}
197+
'
198+
}, 'main.v', 'unsupported generic struct `Phantom`')
154199
}
155200

156201
fn test_generics_allowed_without_building_v() {

0 commit comments

Comments
 (0)