| 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. |
| 4 | module main |
| 5 | |
| 6 | import os |
| 7 | import time |
| 8 | |
| 9 | fn 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 | |