v / cmd / v2 / test_ssa_backends.v
229 lines · 210 sloc · 6.79 KB · b831b0eec9b5b2756784b5dabf3808d47d6a39ae
Raw
1// Copyright (c) 2026 Alexander Medvednikov. All rights reserved.
2// Use of this source code is governed by an MIT license
3// that can be found in the LICENSE file.
4module main
5
6import os
7import time
8
9fn main() {
10 t0 := time.now()
11
12 // Build v2 compiler
13 println('[*] Building v2...')
14 vroot := os.dir(@VEXE)
15 v2_source := os.join_path(vroot, 'cmd', 'v2', 'v2.v')
16 v2_binary := os.join_path(vroot, 'cmd', 'v2', 'v2')
17 build_res := os.execute('${@VEXE} -gc none -cc cc ${v2_source} -o ${v2_binary}')
18 if build_res.exit_code != 0 {
19 eprintln('Error: Failed to build v2')
20 eprintln(build_res.output)
21 exit(1)
22 }
23
24 // Determine backends from command line args.
25 // If none are provided, run the default backend set.
26 mut backends := []string{}
27 if os.args.contains('cleanc') {
28 backends << 'cleanc'
29 }
30 if os.args.contains('c') {
31 backends << 'c'
32 }
33 if os.args.contains('arm64') {
34 backends << 'arm64'
35 }
36 if os.args.contains('x64') {
37 backends << 'x64'
38 }
39 if backends.len == 0 {
40 backends = ['cleanc', 'c', 'arm64']
41 }
42
43 // Parse test file from args or default to test.v
44 // Support: ./test_ssa_backends arm64 path/to/file.v
45 mut input_file := 'test.v'
46 for arg in os.args {
47 if arg.ends_with('.v') && arg != @FILE {
48 input_file = arg
49 break
50 }
51 }
52 if !os.exists(input_file) {
53 eprintln('Error: ${input_file} not found')
54 exit(1)
55 }
56
57 // Derive output binary name from input file
58 base_name := os.file_name(input_file).replace('.v', '')
59 ref_output_path := './.${base_name}_ref.out.tmp'
60 gen_output_path := './.${base_name}_gen.out.tmp'
61
62 // Get expected output: use .out file if --skip-builtin, otherwise run reference compiler
63 mut expected_out := ''
64 out_file := input_file.replace('.v', '.out')
65 if os.args.contains('--skip-builtin') && os.exists(out_file) {
66 println('[*] Using expected output from ${out_file}')
67 expected_out = os.read_file(out_file) or { '' }.trim_space().replace('\r\n', '\n')
68 } else {
69 // Run Reference (v run test.v)
70 println('[*] Running reference: ${@VEXE} -enable-globals run ${input_file}...')
71 os.rm(ref_output_path) or {}
72 ref_cc := if os.user_os() == 'macos' { '-cc cc ' } else { '' }
73 ref_cmd := '${@VEXE} -gc none ${ref_cc}-n -w -enable-globals run ${input_file} > ${ref_output_path} 2>&1'
74 ref_res := os.execute(ref_cmd)
75 ref_out := os.read_file(ref_output_path) or { '' }
76 os.rm(ref_output_path) or {}
77 if ref_res.exit_code != 0 {
78 eprintln('Error: Reference run failed')
79 eprintln(ref_out)
80 exit(1)
81 }
82 // Normalize newlines
83 expected_out = ref_out.trim_space().replace('\r\n', '\n')
84 }
85
86 mut had_failures := false
87 for backend in backends {
88 backend_t0 := time.now()
89
90 // Run v2 with selected backend
91 println('[*] Running v2 -backend ${backend} ${input_file}...')
92 mut backend_flags := '-gc none -backend ${backend}'
93 if backend in ['arm64', 'x64'] {
94 if os.args.contains('-prod') {
95 backend_flags += ' -prod'
96 } else if os.args.contains('-O0') {
97 backend_flags += ' -O0'
98 }
99 }
100 if backend == 'cleanc' {
101 // cleanc needs full per-run codegen for this suite right now.
102 backend_flags += ' -nomarkused -nocache'
103 }
104 if os.args.contains('--skip-builtin') && !backend_flags.contains('--skip-builtin') {
105 backend_flags += ' --skip-builtin'
106 }
107 v2_cmd := '${v2_binary} ${backend_flags} ${input_file} -o ${base_name}'
108 v2_res := os.execute(v2_cmd)
109 if v2_res.exit_code != 0 {
110 eprintln('Error: v2 compilation failed for backend ${backend}')
111 eprintln(v2_res.output)
112 had_failures = true
113 continue
114 }
115 println(v2_res.output)
116 println('compilation took ${time.since(backend_t0)}')
117
118 // Save the v2-produced binary before running another backend (which would overwrite it)
119 saved_binary := './${base_name}_${backend}_v2'
120 os.rm(saved_binary) or {}
121 if os.user_os() == 'windows' {
122 os.rm('${saved_binary}.exe') or {}
123 }
124 os.cp('./${base_name}', saved_binary) or {
125 eprintln('Error: Failed to save v2 binary for backend ${backend}')
126 had_failures = true
127 continue
128 }
129
130 // Run generated binary
131 println('[*] Running generated binary (${backend})...')
132 mut cmd := saved_binary
133 if os.user_os() == 'windows' {
134 cmd = '${saved_binary}.exe'
135 }
136 os.rm(gen_output_path) or {}
137 // 60s timeout to catch infinite loops in ARM64-generated code
138 has_timeout := os.exists('/opt/homebrew/bin/timeout') || os.exists('/usr/bin/timeout')
139 has_gtimeout := os.exists('/opt/homebrew/bin/gtimeout') || os.exists('/usr/bin/gtimeout')
140 timeout_cmd := if has_timeout {
141 'timeout'
142 } else if has_gtimeout {
143 'gtimeout'
144 } else {
145 ''
146 }
147 gen_cmd := if timeout_cmd != '' {
148 '${timeout_cmd} 60 ${cmd} > ${gen_output_path} 2>&1'
149 } else {
150 '${cmd} > ${gen_output_path} 2>&1'
151 }
152 gen_res := os.execute(gen_cmd)
153 gen_out := os.read_file(gen_output_path) or { '' }
154 os.rm(gen_output_path) or {}
155 if gen_res.exit_code != 0 {
156 if gen_res.exit_code == 124 || gen_res.exit_code == 142 || gen_res.exit_code == 14 {
157 eprintln('Error: Execution timed out (infinite loop detected) for backend ${backend}')
158 had_failures = true
159 continue
160 }
161 println('Warning: Binary exited with code ${gen_res.exit_code}')
162 }
163
164 // Strip terminal control characters that script command may prepend
165 mut cleaned := gen_out.replace('\r\n', '\n').replace('\x04', '').replace('\x08', '')
166 // Remove "^D" literal string that macOS script may add
167 if cleaned.starts_with('^D') {
168 cleaned = cleaned[2..]
169 }
170 actual_out := cleaned.trim_space()
171
172 // Compare
173 if expected_out == actual_out {
174 println('\n[SUCCESS] Backend ${backend}: outputs match!')
175 continue
176 }
177
178 had_failures = true
179 println('\n[FAILURE] Backend ${backend}: outputs differ')
180 expected_lines := expected_out.split('\n')
181 actual_lines := actual_out.split('\n')
182
183 // Find first differing line
184 mut first_diff := -1
185 max_lines := if expected_lines.len > actual_lines.len {
186 expected_lines.len
187 } else {
188 actual_lines.len
189 }
190 for i in 0 .. max_lines {
191 exp := if i < expected_lines.len { expected_lines[i] } else { '<missing>' }
192 act := if i < actual_lines.len { actual_lines[i] } else { '<missing>' }
193 if exp != act {
194 first_diff = i
195 break
196 }
197 }
198
199 if first_diff >= 0 {
200 context := 2
201 start := if first_diff > context { first_diff - context } else { 0 }
202 end := if first_diff + context + 1 < max_lines {
203 first_diff + context + 1
204 } else {
205 max_lines
206 }
207
208 println('\nExpected (reference compiler):')
209 for i in start .. end {
210 line := if i < expected_lines.len { expected_lines[i] } else { '<missing>' }
211 println('${i + 1}: ${line}')
212 }
213
214 println('\nGot (v2 ${backend}):')
215 for i in start .. end {
216 line := if i < actual_lines.len { actual_lines[i] } else { '<missing>' }
217 println('${i + 1}: ${line}')
218 }
219 }
220 }
221
222 if had_failures {
223 println('\n[FAILURE] One or more backends failed')
224 exit(1)
225 } else {
226 println('\n[SUCCESS] All requested backends passed')
227 }
228 println('total time ${time.since(t0)}')
229}
230