| 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 | // vtest build: macos |
| 5 | |
| 6 | module abi |
| 7 | |
| 8 | import os |
| 9 | import v2.ast |
| 10 | import v2.mir |
| 11 | import v2.parser |
| 12 | import v2.pref |
| 13 | import v2.ssa |
| 14 | import v2.token |
| 15 | |
| 16 | fn parse_code_for_test(code string) []ast.File { |
| 17 | tmp_file := os.join_path(os.temp_dir(), 'v2_abi_parser_test_${os.getpid()}.v') |
| 18 | os.write_file(tmp_file, code) or { panic(err) } |
| 19 | defer { |
| 20 | os.rm(tmp_file) or {} |
| 21 | } |
| 22 | prefs := &pref.Preferences{} |
| 23 | mut file_set := token.FileSet.new() |
| 24 | mut p := parser.Parser.new(prefs) |
| 25 | return p.parse_files([tmp_file], mut file_set) |
| 26 | } |
| 27 | |
| 28 | fn first_fn_decl(file ast.File) ast.FnDecl { |
| 29 | for stmt in file.stmts { |
| 30 | if stmt is ast.FnDecl { |
| 31 | return stmt |
| 32 | } |
| 33 | } |
| 34 | panic('expected function declaration in parsed file') |
| 35 | } |
| 36 | |
| 37 | fn test_parser_sql_expr_allows_nested_braces_in_body() { |
| 38 | files := parse_code_for_test(" |
| 39 | module main |
| 40 | |
| 41 | fn main() { |
| 42 | req_status := true |
| 43 | sql db { |
| 44 | update User set |
| 45 | name = 'Jengro', |
| 46 | status = if req_status { 1 } else { 0 } |
| 47 | status = fn [req_status] () u8 { if req_status { return 1 } else { return 0 } }() |
| 48 | where id == '100' |
| 49 | } |
| 50 | } |
| 51 | ") |
| 52 | assert files.len == 1 |
| 53 | main_fn := first_fn_decl(files[0]) |
| 54 | assert main_fn.stmts.len == 2 |
| 55 | assert main_fn.stmts[1] is ast.ExprStmt |
| 56 | sql_stmt := main_fn.stmts[1] as ast.ExprStmt |
| 57 | assert sql_stmt.expr is ast.SqlExpr |
| 58 | } |
| 59 | |
| 60 | fn test_arm64_large_struct_call_is_lowered_to_call_sret() { |
| 61 | mut ssa_mod := ssa.Module.new('abi_test') |
| 62 | i64_t := ssa_mod.type_store.get_int(64) |
| 63 | i8_t := ssa_mod.type_store.get_int(8) |
| 64 | ptr_t := ssa_mod.type_store.get_ptr(i8_t) |
| 65 | large_struct_t := ssa_mod.type_store.register(ssa.Type{ |
| 66 | kind: .struct_t |
| 67 | fields: [ptr_t, i64_t, i64_t] // 24 bytes |
| 68 | }) |
| 69 | |
| 70 | callee_id := ssa_mod.new_function('callee', large_struct_t, []) |
| 71 | callee_entry := ssa_mod.add_block(callee_id, 'entry') |
| 72 | zero := ssa_mod.get_or_add_const(i64_t, '0') |
| 73 | ssa_mod.add_instr(.ret, callee_entry, 0, [zero]) |
| 74 | |
| 75 | caller_id := ssa_mod.new_function('caller', i64_t, []) |
| 76 | caller_entry := ssa_mod.add_block(caller_id, 'entry') |
| 77 | fn_val := ssa_mod.add_value_node(.unknown, 0, 'callee', 0) |
| 78 | call_val := ssa_mod.add_instr(.call, caller_entry, large_struct_t, [fn_val]) |
| 79 | ssa_mod.add_instr(.ret, caller_entry, 0, [zero]) |
| 80 | |
| 81 | mut mir_mod := mir.lower_from_ssa(ssa_mod) |
| 82 | lower(mut mir_mod, .arm64) |
| 83 | |
| 84 | call_instr := mir_mod.instrs[mir_mod.values[call_val].index] |
| 85 | assert call_instr.op == .call_sret |
| 86 | } |
| 87 | |
| 88 | fn test_x64_large_struct_call_is_lowered_to_call_sret() { |
| 89 | mut ssa_mod := ssa.Module.new('abi_test_x64') |
| 90 | i64_t := ssa_mod.type_store.get_int(64) |
| 91 | i8_t := ssa_mod.type_store.get_int(8) |
| 92 | ptr_t := ssa_mod.type_store.get_ptr(i8_t) |
| 93 | large_struct_t := ssa_mod.type_store.register(ssa.Type{ |
| 94 | kind: .struct_t |
| 95 | fields: [ptr_t, i64_t, i64_t] |
| 96 | }) |
| 97 | |
| 98 | callee_id := ssa_mod.new_function('callee', large_struct_t, []) |
| 99 | callee_entry := ssa_mod.add_block(callee_id, 'entry') |
| 100 | zero := ssa_mod.get_or_add_const(i64_t, '0') |
| 101 | ssa_mod.add_instr(.ret, callee_entry, 0, [zero]) |
| 102 | |
| 103 | caller_id := ssa_mod.new_function('caller', i64_t, []) |
| 104 | caller_entry := ssa_mod.add_block(caller_id, 'entry') |
| 105 | fn_val := ssa_mod.add_value_node(.unknown, 0, 'callee', 0) |
| 106 | call_val := ssa_mod.add_instr(.call, caller_entry, large_struct_t, [fn_val]) |
| 107 | ssa_mod.add_instr(.ret, caller_entry, 0, [zero]) |
| 108 | |
| 109 | mut mir_mod := mir.lower_from_ssa(ssa_mod) |
| 110 | lower(mut mir_mod, .x64) |
| 111 | |
| 112 | call_instr := mir_mod.instrs[mir_mod.values[call_val].index] |
| 113 | assert call_instr.op == .call_sret |
| 114 | } |
| 115 | |
| 116 | fn test_x64_sysv_sixteen_byte_struct_return_stays_direct() { |
| 117 | mut ssa_mod := ssa.Module.new('abi_test_x64_sysv_16_ret') |
| 118 | i64_t := ssa_mod.type_store.get_int(64) |
| 119 | struct16_t := ssa_mod.type_store.register(ssa.Type{ |
| 120 | kind: .struct_t |
| 121 | fields: [i64_t, i64_t] |
| 122 | }) |
| 123 | |
| 124 | callee_id := ssa_mod.new_function('callee', struct16_t, []) |
| 125 | callee_entry := ssa_mod.add_block(callee_id, 'entry') |
| 126 | zero := ssa_mod.get_or_add_const(i64_t, '0') |
| 127 | ssa_mod.add_instr(.ret, callee_entry, 0, [zero]) |
| 128 | |
| 129 | caller_id := ssa_mod.new_function('caller', i64_t, []) |
| 130 | caller_entry := ssa_mod.add_block(caller_id, 'entry') |
| 131 | fn_val := ssa_mod.add_value_node(.unknown, 0, 'callee', 0) |
| 132 | call_val := ssa_mod.add_instr(.call, caller_entry, struct16_t, [fn_val]) |
| 133 | ssa_mod.add_instr(.ret, caller_entry, 0, [zero]) |
| 134 | |
| 135 | mut mir_mod := mir.lower_from_ssa(ssa_mod) |
| 136 | lower(mut mir_mod, .x64) |
| 137 | |
| 138 | call_instr := mir_mod.instrs[mir_mod.values[call_val].index] |
| 139 | assert call_instr.op == .call |
| 140 | assert !call_instr.abi_ret_indirect |
| 141 | } |
| 142 | |
| 143 | fn test_x64_windows_struct_return_classification() { |
| 144 | mut ssa_mod := ssa.Module.new('abi_test_x64_windows_ret') |
| 145 | i64_t := ssa_mod.type_store.get_int(64) |
| 146 | i8_t := ssa_mod.type_store.get_int(8) |
| 147 | struct8_t := ssa_mod.type_store.register(ssa.Type{ |
| 148 | kind: .struct_t |
| 149 | fields: [i64_t] |
| 150 | }) |
| 151 | struct9_t := ssa_mod.type_store.register(ssa.Type{ |
| 152 | kind: .struct_t |
| 153 | fields: [i64_t, i8_t] |
| 154 | }) |
| 155 | struct16_t := ssa_mod.type_store.register(ssa.Type{ |
| 156 | kind: .struct_t |
| 157 | fields: [i64_t, i64_t] |
| 158 | }) |
| 159 | |
| 160 | fn8 := ssa_mod.new_function('ret8', struct8_t, []) |
| 161 | fn9 := ssa_mod.new_function('ret9', struct9_t, []) |
| 162 | fn16 := ssa_mod.new_function('ret16', struct16_t, []) |
| 163 | |
| 164 | mut mir_mod := mir.lower_from_ssa(ssa_mod) |
| 165 | lower_with_x64_abi(mut mir_mod, .x64, .windows) |
| 166 | |
| 167 | assert !mir_mod.funcs[fn8].abi_ret_indirect |
| 168 | assert mir_mod.funcs[fn9].abi_ret_indirect |
| 169 | assert mir_mod.funcs[fn16].abi_ret_indirect |
| 170 | } |
| 171 | |
| 172 | fn test_x64_windows_struct_param_classification() { |
| 173 | mut ssa_mod := ssa.Module.new('abi_test_x64_windows_param') |
| 174 | i64_t := ssa_mod.type_store.get_int(64) |
| 175 | i8_t := ssa_mod.type_store.get_int(8) |
| 176 | struct8_t := ssa_mod.type_store.register(ssa.Type{ |
| 177 | kind: .struct_t |
| 178 | fields: [i64_t] |
| 179 | }) |
| 180 | struct9_t := ssa_mod.type_store.register(ssa.Type{ |
| 181 | kind: .struct_t |
| 182 | fields: [i64_t, i8_t] |
| 183 | }) |
| 184 | struct16_t := ssa_mod.type_store.register(ssa.Type{ |
| 185 | kind: .struct_t |
| 186 | fields: [i64_t, i64_t] |
| 187 | }) |
| 188 | |
| 189 | fn_id := ssa_mod.new_function('callee', i64_t, []) |
| 190 | param8 := ssa_mod.add_value_node(.argument, struct8_t, 's8', 0) |
| 191 | param9 := ssa_mod.add_value_node(.argument, struct9_t, 's9', 0) |
| 192 | param16 := ssa_mod.add_value_node(.argument, struct16_t, 's16', 0) |
| 193 | ssa_mod.funcs[fn_id].params << [param8, param9, param16] |
| 194 | |
| 195 | mut mir_mod := mir.lower_from_ssa(ssa_mod) |
| 196 | lower_with_x64_abi(mut mir_mod, .x64, .windows) |
| 197 | |
| 198 | assert mir_mod.funcs[fn_id].abi_param_class == [.in_reg, .indirect, .indirect] |
| 199 | } |
| 200 | |
| 201 | fn test_x64_windows_callsite_struct_arg_classification() { |
| 202 | mut ssa_mod := ssa.Module.new('abi_test_x64_windows_call_arg') |
| 203 | i64_t := ssa_mod.type_store.get_int(64) |
| 204 | i8_t := ssa_mod.type_store.get_int(8) |
| 205 | struct8_t := ssa_mod.type_store.register(ssa.Type{ |
| 206 | kind: .struct_t |
| 207 | fields: [i64_t] |
| 208 | }) |
| 209 | struct9_t := ssa_mod.type_store.register(ssa.Type{ |
| 210 | kind: .struct_t |
| 211 | fields: [i64_t, i8_t] |
| 212 | }) |
| 213 | struct16_t := ssa_mod.type_store.register(ssa.Type{ |
| 214 | kind: .struct_t |
| 215 | fields: [i64_t, i64_t] |
| 216 | }) |
| 217 | |
| 218 | callee_id := ssa_mod.new_function('callee', i64_t, []) |
| 219 | param8 := ssa_mod.add_value_node(.argument, struct8_t, 's8', 0) |
| 220 | param9 := ssa_mod.add_value_node(.argument, struct9_t, 's9', 0) |
| 221 | param16 := ssa_mod.add_value_node(.argument, struct16_t, 's16', 0) |
| 222 | ssa_mod.funcs[callee_id].params << [param8, param9, param16] |
| 223 | |
| 224 | caller_id := ssa_mod.new_function('caller', i64_t, []) |
| 225 | arg8 := ssa_mod.add_value_node(.argument, struct8_t, 'arg8', 0) |
| 226 | arg9 := ssa_mod.add_value_node(.argument, struct9_t, 'arg9', 0) |
| 227 | arg16 := ssa_mod.add_value_node(.argument, struct16_t, 'arg16', 0) |
| 228 | ssa_mod.funcs[caller_id].params << [arg8, arg9, arg16] |
| 229 | caller_entry := ssa_mod.add_block(caller_id, 'entry') |
| 230 | fn_val := ssa_mod.add_value_node(.unknown, 0, 'callee', 0) |
| 231 | call_val := ssa_mod.add_instr(.call, caller_entry, i64_t, [fn_val, arg8, arg9, arg16]) |
| 232 | zero := ssa_mod.get_or_add_const(i64_t, '0') |
| 233 | ssa_mod.add_instr(.ret, caller_entry, 0, [zero]) |
| 234 | |
| 235 | mut mir_mod := mir.lower_from_ssa(ssa_mod) |
| 236 | lower_with_x64_abi(mut mir_mod, .x64, .windows) |
| 237 | |
| 238 | call_instr := mir_mod.instrs[mir_mod.values[call_val].index] |
| 239 | assert call_instr.abi_arg_class == [.in_reg, .indirect, .indirect] |
| 240 | } |
| 241 | |
| 242 | fn test_arm64_external_large_struct_call_is_lowered_to_call_sret() { |
| 243 | mut ssa_mod := ssa.Module.new('abi_test_external') |
| 244 | i64_t := ssa_mod.type_store.get_int(64) |
| 245 | i8_t := ssa_mod.type_store.get_int(8) |
| 246 | ptr_t := ssa_mod.type_store.get_ptr(i8_t) |
| 247 | large_struct_t := ssa_mod.type_store.register(ssa.Type{ |
| 248 | kind: .struct_t |
| 249 | fields: [ptr_t, i64_t, i64_t] |
| 250 | }) |
| 251 | |
| 252 | caller_id := ssa_mod.new_function('caller', i64_t, []) |
| 253 | caller_entry := ssa_mod.add_block(caller_id, 'entry') |
| 254 | external_fn := ssa_mod.add_value_node(.unknown, 0, 'external_make', 0) |
| 255 | call_val := ssa_mod.add_instr(.call, caller_entry, large_struct_t, [external_fn]) |
| 256 | zero := ssa_mod.get_or_add_const(i64_t, '0') |
| 257 | ssa_mod.add_instr(.ret, caller_entry, 0, [zero]) |
| 258 | |
| 259 | mut mir_mod := mir.lower_from_ssa(ssa_mod) |
| 260 | lower(mut mir_mod, .arm64) |
| 261 | |
| 262 | call_instr := mir_mod.instrs[mir_mod.values[call_val].index] |
| 263 | assert call_instr.op == .call_sret |
| 264 | } |
| 265 | |
| 266 | fn test_arm64_indirect_call_large_struct_return_is_lowered_to_call_sret() { |
| 267 | mut ssa_mod := ssa.Module.new('abi_test_indirect') |
| 268 | i64_t := ssa_mod.type_store.get_int(64) |
| 269 | i8_t := ssa_mod.type_store.get_int(8) |
| 270 | ptr_t := ssa_mod.type_store.get_ptr(i8_t) |
| 271 | large_struct_t := ssa_mod.type_store.register(ssa.Type{ |
| 272 | kind: .struct_t |
| 273 | fields: [ptr_t, i64_t, i64_t] |
| 274 | }) |
| 275 | fn_sig_t := ssa_mod.type_store.register(ssa.Type{ |
| 276 | kind: .func_t |
| 277 | params: []ssa.TypeID{} |
| 278 | ret_type: large_struct_t |
| 279 | }) |
| 280 | fn_ptr_t := ssa_mod.type_store.get_ptr(fn_sig_t) |
| 281 | |
| 282 | caller_id := ssa_mod.new_function('caller', i64_t, []) |
| 283 | fn_ptr := ssa_mod.add_value_node(.argument, fn_ptr_t, 'fn_ptr', 0) |
| 284 | ssa_mod.funcs[caller_id].params << fn_ptr |
| 285 | caller_entry := ssa_mod.add_block(caller_id, 'entry') |
| 286 | call_val := ssa_mod.add_instr(.call_indirect, caller_entry, large_struct_t, [ |
| 287 | fn_ptr, |
| 288 | ]) |
| 289 | zero := ssa_mod.get_or_add_const(i64_t, '0') |
| 290 | ssa_mod.add_instr(.ret, caller_entry, 0, [zero]) |
| 291 | |
| 292 | mut mir_mod := mir.lower_from_ssa(ssa_mod) |
| 293 | lower(mut mir_mod, .arm64) |
| 294 | |
| 295 | call_instr := mir_mod.instrs[mir_mod.values[call_val].index] |
| 296 | assert call_instr.op == .call_sret |
| 297 | } |
| 298 | |
| 299 | fn test_arm64_callsite_marks_large_struct_arg_indirect() { |
| 300 | mut ssa_mod := ssa.Module.new('abi_test_arg_class') |
| 301 | i64_t := ssa_mod.type_store.get_int(64) |
| 302 | i8_t := ssa_mod.type_store.get_int(8) |
| 303 | ptr_t := ssa_mod.type_store.get_ptr(i8_t) |
| 304 | large_struct_t := ssa_mod.type_store.register(ssa.Type{ |
| 305 | kind: .struct_t |
| 306 | fields: [ptr_t, i64_t, i64_t] |
| 307 | }) |
| 308 | |
| 309 | callee_id := ssa_mod.new_function('callee', i64_t, []) |
| 310 | callee_param := ssa_mod.add_value_node(.argument, large_struct_t, 'arg', 0) |
| 311 | ssa_mod.funcs[callee_id].params << callee_param |
| 312 | callee_entry := ssa_mod.add_block(callee_id, 'entry') |
| 313 | zero := ssa_mod.get_or_add_const(i64_t, '0') |
| 314 | ssa_mod.add_instr(.ret, callee_entry, 0, [zero]) |
| 315 | |
| 316 | caller_id := ssa_mod.new_function('caller', i64_t, []) |
| 317 | caller_param := ssa_mod.add_value_node(.argument, large_struct_t, 'in', 0) |
| 318 | ssa_mod.funcs[caller_id].params << caller_param |
| 319 | caller_entry := ssa_mod.add_block(caller_id, 'entry') |
| 320 | fn_val := ssa_mod.add_value_node(.unknown, 0, 'callee', 0) |
| 321 | call_val := ssa_mod.add_instr(.call, caller_entry, i64_t, [fn_val, caller_param]) |
| 322 | ssa_mod.add_instr(.ret, caller_entry, 0, [zero]) |
| 323 | |
| 324 | mut mir_mod := mir.lower_from_ssa(ssa_mod) |
| 325 | lower(mut mir_mod, .arm64) |
| 326 | |
| 327 | call_instr := mir_mod.instrs[mir_mod.values[call_val].index] |
| 328 | assert call_instr.abi_arg_class.len == 1 |
| 329 | assert call_instr.abi_arg_class[0] == .indirect |
| 330 | } |
| 331 | |
| 332 | fn test_arm64_callsite_marks_large_array_arg_indirect() { |
| 333 | mut ssa_mod := ssa.Module.new('abi_test_array_arg_class') |
| 334 | i64_t := ssa_mod.type_store.get_int(64) |
| 335 | large_array_t := ssa_mod.type_store.register(ssa.Type{ |
| 336 | kind: .array_t |
| 337 | elem_type: i64_t |
| 338 | len: 3 |
| 339 | }) |
| 340 | |
| 341 | callee_id := ssa_mod.new_function('callee', i64_t, []) |
| 342 | callee_param := ssa_mod.add_value_node(.argument, large_array_t, 'arg', 0) |
| 343 | ssa_mod.funcs[callee_id].params << callee_param |
| 344 | callee_entry := ssa_mod.add_block(callee_id, 'entry') |
| 345 | zero := ssa_mod.get_or_add_const(i64_t, '0') |
| 346 | ssa_mod.add_instr(.ret, callee_entry, 0, [zero]) |
| 347 | |
| 348 | caller_id := ssa_mod.new_function('caller', i64_t, []) |
| 349 | caller_param := ssa_mod.add_value_node(.argument, large_array_t, 'in', 0) |
| 350 | ssa_mod.funcs[caller_id].params << caller_param |
| 351 | caller_entry := ssa_mod.add_block(caller_id, 'entry') |
| 352 | fn_val := ssa_mod.add_value_node(.unknown, 0, 'callee', 0) |
| 353 | call_val := ssa_mod.add_instr(.call, caller_entry, i64_t, [fn_val, caller_param]) |
| 354 | ssa_mod.add_instr(.ret, caller_entry, 0, [zero]) |
| 355 | |
| 356 | mut mir_mod := mir.lower_from_ssa(ssa_mod) |
| 357 | lower(mut mir_mod, .arm64) |
| 358 | |
| 359 | call_instr := mir_mod.instrs[mir_mod.values[call_val].index] |
| 360 | assert call_instr.abi_arg_class.len == 1 |
| 361 | assert call_instr.abi_arg_class[0] == .indirect |
| 362 | } |
| 363 | |
| 364 | fn test_arm64_unresolved_callsite_marks_large_array_alloca_arg_indirect() { |
| 365 | mut ssa_mod := ssa.Module.new('abi_test_array_alloca_arg_class') |
| 366 | i64_t := ssa_mod.type_store.get_int(64) |
| 367 | large_array_t := ssa_mod.type_store.register(ssa.Type{ |
| 368 | kind: .array_t |
| 369 | elem_type: i64_t |
| 370 | len: 3 |
| 371 | }) |
| 372 | large_array_ptr_t := ssa_mod.type_store.get_ptr(large_array_t) |
| 373 | |
| 374 | caller_id := ssa_mod.new_function('caller', i64_t, []) |
| 375 | caller_entry := ssa_mod.add_block(caller_id, 'entry') |
| 376 | tmp := ssa_mod.add_instr(.alloca, caller_entry, large_array_ptr_t, []ssa.ValueID{}) |
| 377 | external_fn := ssa_mod.add_value_node(.unknown, 0, 'external_append_like', 0) |
| 378 | call_val := ssa_mod.add_instr(.call, caller_entry, i64_t, [external_fn, tmp]) |
| 379 | zero := ssa_mod.get_or_add_const(i64_t, '0') |
| 380 | ssa_mod.add_instr(.ret, caller_entry, 0, [zero]) |
| 381 | |
| 382 | mut mir_mod := mir.lower_from_ssa(ssa_mod) |
| 383 | lower(mut mir_mod, .arm64) |
| 384 | |
| 385 | call_instr := mir_mod.instrs[mir_mod.values[call_val].index] |
| 386 | assert call_instr.abi_arg_class.len == 1 |
| 387 | assert call_instr.abi_arg_class[0] == .indirect |
| 388 | } |
| 389 | |