1#!/usr/bin/env ruby 2# Copyright (c) 2021-2024 Huawei Device Co., Ltd. 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15require 'optparse' 16require 'ostruct' 17require 'logger' 18require 'fileutils' 19require 'open3' 20 21options = OpenStruct.new 22OptionParser.new do |opts| 23 opts.banner = 'Usage: checker.rb [options] TEST_FILE' 24 25 opts.on('--run-prefix=PREFIX', 'Prefix that will be inserted before panda run command') do |v| 26 options.run_prefix = v 27 end 28 opts.on('--source=FILE', 'Path to source file') 29 opts.on('--test-file=FILE', 'Path to test file') do |v| 30 options.test_file = v 31 end 32 opts.on('--panda=PANDA', 'Path to panda') 33 opts.on('--paoc=PAOC', 'Path to paoc') do |v| 34 options.paoc = v 35 end 36 opts.on('--frontend=FRONTEND', 'Path to frontend binary') 37 opts.on('--panda-options=OPTIONS', 'Default options for panda run') do |v| 38 options.panda_options = v 39 end 40 opts.on('--paoc-options=OPTIONS', 'Default options for paoc run') do |v| 41 options.paoc_options = v 42 end 43 opts.on('--frontend-options=OPTIONS', 'Default options for frontend+bco run') do |v| 44 options.frontend_options = v 45 end 46 opts.on('--method=METHOD', 'Method to optimize') 47 opts.on('--command-token=STRING', 'String that is recognized as command start') do |v| 48 options.command_token = v 49 end 50 opts.on('--release', 'Run in release mode. EVENT, INST and other will not be checked') 51 opts.on('-v', '--verbose', 'Verbose logging') 52 opts.on('--arch=ARCHITECTURE', 'Architecture of system where start panda') 53 opts.on("--keep-data", "Do not remove generated data from disk") { |v| options.keep_data = true } 54 opts.on("--with-llvm", "Tells checker that ARK was built with LLVM support") do |v| 55 options.with_llvm = true 56 end 57end.parse!(into: options) 58 59$LOG_LEVEL = options.verbose ? Logger::DEBUG : Logger::ERROR 60$curr_cmd = nil 61 62def log 63 @log ||= Logger.new($stdout, level: $LOG_LEVEL) 64end 65 66def raise_error(msg) 67 log.error "Test failed: #{$checker_name}" 68 if !$current_method.nil? 69 log.error "Method: \"#{$current_method}\"" 70 end 71 if !$current_pass.nil? 72 log.error $current_pass 73 end 74 log.error msg 75 log.error "Command to reproduce: #{$curr_cmd}" 76 raise msg 77end 78 79def match_str(match) 80 match.is_a?(Regexp) ? "/#{match.source}/" : match 81end 82 83def contains?(str, match) 84 return str =~ match if match.is_a? Regexp 85 86 raise_error "Wrong type for search: #{match.class}" unless match.is_a? String 87 str.include? match 88end 89 90# Provides methods to search lines in a given array 91class SearchScope 92 93 attr_reader :lines 94 95 def initialize(lines, name) 96 @lines = lines 97 @name = name 98 @current_index = 0 99 end 100 101 def find_method_dump(match) 102 @lines = @lines.drop(@current_index) 103 @current_index = 0 104 find(match) 105 @lines = @lines.drop(@current_index - 1) 106 @current_index = 0 107 find(/}$/) 108 @lines = @lines.slice(0, @current_index) 109 @current_index = 0 110 end 111 112 def find_block(match) 113 @lines = @lines.drop(@current_index) 114 @current_index = 0 115 find(match) 116 @lines = @lines.drop(@current_index - 1) 117 @current_index = 0 118 find(/succs:/) 119 @lines = @lines.slice(0, @current_index) 120 @current_index = 0 121 end 122 123 def self.from_file(fname, name) 124 SearchScope.new(File.readlines(fname), name) 125 end 126 127 def find(match) 128 return if match.nil? 129 130 @current_index = @lines.index { |line| contains?(line, match) } 131 raise_error "#{@name} not found: #{match_str(match)}" if @current_index.nil? 132 @current_index += 1 133 end 134 135 def find_next(match) 136 return if match.nil? 137 138 index = @lines.drop(@current_index).index { |line| contains?(line, match) } 139 raise_error "#{@name} not found: #{match_str(match)}" if index.nil? 140 @current_index += index + 1 141 end 142 143 def find_not(match) 144 return if match.nil? 145 146 @lines.each do |line| 147 raise_error "#{@name} should not occur: #{match_str(match)}" if contains?(line, match) 148 end 149 end 150 151 def find_next_not(match) 152 return if match.nil? 153 154 @lines.drop(@current_index).each do |line| 155 raise_error "#{@name} should not occur: #{match_str(match)}" if contains?(line, match) 156 end 157 end 158 159 def to_s 160 "Scope '#{@name}', current=#{@current_index}\n#{@lines.join}" 161 end 162end 163 164class Checker 165 attr_reader :name 166 167 def initialize(options, name) 168 @name = name 169 @lines = [] 170 @code = "" 171 @cwd = "#{Dir.getwd}/#{name.gsub(/[ -:()]/, '_')}" 172 @options = options 173 @args = '' 174 @ir_files = [] 175 @architecture = options.arch 176 @aot_file = '' 177 @llvm_paoc = false 178 179 # Events scope for 'events.csv' 180 @events_scope = nil 181 # IR scope for IR dumps files 'ir_dump/*.ir' 182 @ir_scope = nil 183 184 # Disassembly file lines, that were read from 'disasm.txt' 185 @disasm_lines = nil 186 # Currently processing disasm method 187 @disasm_method_scope = nil 188 # Current search scope 189 @disasm_scope = nil 190 191 Dir.mkdir(@cwd) unless File.exists?(@cwd) 192 clear_data 193 end 194 195 def set_llvm_paoc() 196 @llvm_paoc = true 197 end 198 199 def append_line(line) 200 @code << line 201 end 202 203 def RUN(**args) 204 expected_result = 0 205 aborted_sig = 0 206 entry = '_GLOBAL::main' 207 env = '' 208 @args = '' 209 args.each do |name, value| 210 if name == :force_jit and value 211 @args << '--compiler-hotness-threshold=0 --no-async-jit=true --compiler-enable-jit=true ' 212 elsif name == :options 213 @args << value 214 elsif name == :entry 215 entry = value 216 elsif name == :result 217 expected_result = value 218 elsif name == :abort 219 aborted_sig = value 220 elsif name == :env 221 env = value 222 end 223 end 224 raise ":abort and :result cannot be set at the same time, :abort = #{aborted_sig}, :result = #{expected_result}" if aborted_sig != 0 && expected_result != 0 225 226 clear_data 227 aot_arg = @aot_file.empty? ? '' : "--aot-file #{@aot_file}" 228 229 cmd = "#{@options.run_prefix} #{@options.panda} --compiler-queue-type=simple --compiler-ignore-failures=false #{@options.panda_options} \ 230 #{aot_arg} #{@args} --events-output=csv --compiler-dump --compiler-disasm-dump:single-file #{@options.test_file} #{entry}" 231 $curr_cmd = "#{env} #{cmd}" 232 log.debug "Panda command: #{$curr_cmd}" 233 234 # See note on exec in RUN_PAOC 235 output, status = Open3.capture2e("#{env} exec #{cmd}", chdir: @cwd.to_s) 236 if aborted_sig != 0 && !status.signaled? 237 puts output 238 log.error "Expected ark to abort with signal #{aborted_sig}, but ark did not signal" 239 raise_error "Test '#{@name}' failed" 240 end 241 if status.signaled? 242 if status.termsig != aborted_sig 243 puts output 244 log.error "ark aborted with signal #{status.termsig}, but expected #{aborted_sig}" 245 raise_error "Test '#{@name}' failed" 246 end 247 elsif status.exitstatus != expected_result 248 puts output 249 log.error "ark returns code #{status.exitstatus}, but expected #{expected_result}" 250 raise_error "Test '#{@name}' failed" 251 end 252 log.debug output 253 File.open("#{@cwd}/console.out", "w") { |file| file.write(output) } 254 255 @events_scope = SearchScope.from_file("#{@cwd}/events.csv", 'Events') 256 end 257 258 def RUN_PAOC(**args) 259 @aot_file = "#{Dir.getwd}/#{File.basename(@options.test_file, File.extname(@options.test_file))}.an" 260 261 inputs = @options.test_file 262 aot_output_option = '--paoc-output' 263 output = @aot_file 264 options = '' 265 env = '' 266 aborted_sig = 0 267 result = 0 268 269 args.each do |name, value| 270 case name 271 when :options 272 options = value 273 when :boot 274 aot_output_option = '--paoc-boot-output' 275 when :env 276 env = value 277 when :inputs 278 inputs = value 279 when :abort 280 aborted_sig = value 281 when :output 282 output = value 283 when :result 284 result = value 285 end 286 end 287 raise ":abort and :result cannot be set at the same time, :abort = #{aborted_sig}, :result = #{result}" if aborted_sig != 0 && result != 0 288 289 paoc_args = "--paoc-panda-files #{inputs} --events-output=csv --compiler-dump #{options} #{aot_output_option} #{output}" 290 291 clear_data 292 293 cmd = "#{@options.run_prefix} #{@options.paoc} --compiler-ignore-failures=false --compiler-disasm-dump:single-file --compiler-dump #{@options.paoc_options} #{paoc_args}" 294 $curr_cmd = "#{env} #{cmd}" 295 log.debug "Paoc command: #{$curr_cmd}" 296 297 # Using exec to pass signal info to the parent process. 298 # Ruby invokes a process using /bin/sh if the curr_cmd has a metacharacter in it, for example '*', '?', '$'. 299 # If an invoked process signals, then the status.signaled? check below returns different values depending on the shell. 300 # For bash it is true, for dash it is false, because bash propagates a flag, whether the process has signalled or not. 301 # When we use 'exec' we will propagate the signal too 302 output, status = Open3.capture2e("#{env} exec #{cmd}", chdir: @cwd.to_s) 303 if aborted_sig != 0 && !status.signaled? 304 puts output 305 log.error "Expected ark_aot to abort with signal #{aborted_sig}, but ark_aot did not signal" 306 raise_error "Test '#{@name}' failed" 307 end 308 if status.signaled? 309 if status.termsig != aborted_sig 310 puts output 311 log.error "ark_aot aborted with signal #{status.termsig}, but expected #{aborted_sig}" 312 raise_error "Test '#{@name}' failed" 313 end 314 elsif status.exitstatus != result 315 puts output 316 log.error "ark_aot returns code #{status.exitstatus}, but expected #{result}" 317 raise_error "Test '#{@name}' failed" 318 end 319 log.debug output 320 File.open("#{@cwd}/console.out", "w") { |file| file.write(output) } 321 322 @events_scope = SearchScope.from_file("#{@cwd}/events.csv", 'Events') 323 end 324 325 def RUN_AOT(**args) 326 if @llvm_paoc 327 RUN_LLVM(**args) 328 else 329 RUN_PAOC(**args) 330 end 331 end 332 333 def RUN_BCO(**args) 334 inputs = @options.test_file 335 output = "#{@cwd}/#{File.basename(@options.test_file, '.*')}.abc" 336 @args = '' 337 338 args.each do |name, value| 339 case name 340 when :options 341 @args << value 342 when :inputs 343 inputs = value 344 when :output 345 output = value 346 when :method 347 @args << "--bco-optimizer --method-regex=#{value}:.*" 348 end 349 end 350 351 clear_data 352 $curr_cmd = "#{@options.frontend} --opt-level=2 --dump-assembly --bco-compiler --compiler-dump \ 353 #{@options.frontend_options} #{@args} --output=#{output} #{@options.source}" 354 log.debug "Frontend command: #{$curr_cmd}" 355 356 # See note on exec in RUN_PAOC 357 output, err_output, status = Open3.capture3("exec #{$curr_cmd}", chdir: @cwd.to_s) 358 if status.signaled? 359 if status.termsig != 0 360 puts output 361 log.error "#{@options.frontend} aborted with signal #{status.termsig}, but expected 0" 362 raise_error "Test '#{@name}' failed" 363 end 364 elsif status.exitstatus != 0 365 puts output 366 log.error "#{@options.frontend} returns code #{status.exitstatus}, but expected 0" 367 raise_error "Test '#{@name}' failed" 368 elsif !err_output.empty? 369 log.error "Bytecode optimizer failed, logs:" 370 puts err_output 371 raise_error "Test '#{@name}' failed" 372 end 373 File.open("#{@cwd}/console.out", "w") { |file| file.write(output) } 374 Open3.capture2e("cat #{@cwd}/console.out") 375 FileUtils.touch("#{@cwd}/events.csv") 376 end 377 378 def RUN_LLVM(**args) 379 raise SkipException unless @options.with_llvm 380 381 args[:options] << " --paoc-mode=llvm " 382 RUN_PAOC(**args) 383 end 384 385 def EVENT(match) 386 return if @options.release 387 388 @events_scope.find(match) 389 end 390 391 def EVENT_NEXT(match) 392 return if @options.release 393 394 @events_scope.find_next(match) 395 end 396 397 def EVENT_COUNT(match) 398 return 0 if @options.release 399 400 @events_scope.lines.count { |event| contains?(event, match) } 401 end 402 403 def EVENT_NOT(match) 404 return if @options.release 405 406 @events_scope.find_not(match) 407 end 408 409 def EVENT_NEXT_NOT(match) 410 return if @options.release 411 412 @events_scope.find_next_not(match) 413 end 414 415 def EVENTS_COUNT(match, count) 416 return if @options.release 417 418 res = @events_scope.lines.count { |event| contains?(event, match) } 419 raise_error "Events count missmatch for #{match}, expected: #{count}, real: #{res}" unless res == count 420 end 421 422 def TRUE(condition) 423 return if @options.release 424 425 raise_error "Not true condition: \"#{condition}\"" unless condition 426 end 427 428 class SkipException < StandardError 429 end 430 431 def SKIP_IF(condition) 432 raise SkipException if condition 433 end 434 435 def IR_COUNT(match) 436 return 0 if @options.release 437 438 @ir_scope.lines.count { |inst| contains?(inst, match) && !contains?(inst, /^Method:/) } 439 end 440 441 def BLOCK_COUNT 442 IR_COUNT('BB ') 443 end 444 445 def INST(match) 446 return if @options.release 447 448 @ir_scope.find(match) 449 end 450 451 def INST_NEXT(match) 452 return if @options.release 453 454 @ir_scope.find_next(match) 455 end 456 457 def INST_NOT(match) 458 return if @options.release 459 @ir_scope.find_not(match) 460 end 461 462 def INST_NEXT_NOT(match) 463 return if @options.release 464 465 @ir_scope.find_next_not(match) 466 end 467 468 def INST_COUNT(match, count) 469 return if @options.release 470 471 real_count = IR_COUNT(match) 472 raise_error "IR_COUNT mismatch for #{match}: expected=#{count}, real=#{real_count}" unless real_count == count 473 end 474 475 def IN_BLOCK(match) 476 return if @options.release 477 478 @ir_scope.find_block(/prop: #{match}/) 479 end 480 481 def LLVM_METHOD(match) 482 return if @options.release 483 484 @ir_scope.find_method_dump(match) 485 end 486 487 def BC_METHOD(match) 488 return if @options.release 489 490 READ_FILE "console.out" 491 @ir_scope.find_method_dump(/^\.function.*#{match.gsub('.', '-')}/) 492 end 493 494 module SearchState 495 NONE = 0 496 SEARCH_BODY = 1 497 SEARCH_END = 2 498 end 499 500 def ASM_METHOD(match) 501 ensure_disasm 502 state = SearchState::NONE 503 start_index = nil 504 end_index = -1 505 @disasm_lines.each_with_index do |line, index| 506 case state 507 when SearchState::NONE 508 if line.start_with?('METHOD_INFO:') && contains?(@disasm_lines[index + 1].split(':', 2)[1].strip, match) 509 state = SearchState::SEARCH_BODY 510 end 511 when SearchState::SEARCH_BODY 512 if line.start_with?('DISASSEMBLY') 513 start_index = index + 1 514 state = SearchState::SEARCH_END 515 end 516 when SearchState::SEARCH_END 517 if line.start_with?('METHOD_INFO:') 518 end_index = index - 1 519 break 520 end 521 end 522 end 523 raise "Method not found: #{match_str(match)}" if start_index.nil? 524 525 @disasm_method_scope = SearchScope.new(@disasm_lines[start_index..end_index], "Method: #{match_str(match)}") 526 @disasm_scope = @disasm_method_scope 527 end 528 529 def ASM_INST(match) 530 ensure_disasm 531 state = SearchState::NONE 532 start_index = nil 533 end_index = -1 534 prefix = nil 535 @disasm_method_scope.lines.each_with_index do |line, index| 536 case state 537 when SearchState::NONE 538 if contains?(line, match) 539 prefix = line.sub(/#.*/, '#').gsub("\n", '') 540 start_index = index + 1 541 state = SearchState::SEARCH_END 542 end 543 when SearchState::SEARCH_END 544 if line.start_with?(prefix) 545 end_index = index - 1 546 break 547 end 548 end 549 end 550 raise "Can not find asm instruction: #{match}" if start_index.nil? 551 552 @disasm_scope = SearchScope.new(@disasm_method_scope.lines[start_index..end_index], "Inst: #{match_str(match)}") 553 end 554 555 def ASM_RESET 556 @disasm_scope = @disasm_method_scope 557 end 558 559 def ASM(**kwargs) 560 ensure_disasm 561 @disasm_scope.find(select_asm(kwargs)) 562 end 563 564 def ASM_NEXT(**kwargs) 565 ensure_disasm 566 @disasm_scope.find_next(select_asm(kwargs)) 567 end 568 569 def ASM_NOT(**kwargs) 570 ensure_disasm 571 @disasm_scope.find_not(select_asm(kwargs)) 572 end 573 574 def ASM_NEXT_NOT(**kwargs) 575 ensure_disasm 576 @disasm_scope.find_next_not(select_asm(kwargs)) 577 end 578 579 def select_asm(kwargs) 580 kwargs[@options.arch.to_sym] 581 end 582 583 def ensure_disasm 584 @disasm_lines ||= File.readlines("#{@cwd}/disasm.txt") 585 end 586 587 def METHOD(method) 588 return if @options.release 589 590 @ir_files = Dir["#{@cwd}/ir_dump/*#{method.gsub(/::|[<>]/, '_')}*.ir"] 591 @ir_files.sort! 592 raise_error "IR dumps not found for method: #{method.gsub(/::|[<>]/, '_')}" if @ir_files.empty? 593 $current_method = method 594 @current_file_index = 0 595 end 596 597 def PASS_AFTER(pass) 598 return if @options.release 599 600 $current_pass = "Pass after: #{pass}" 601 @current_file_index = @ir_files.index { |x| File.basename(x).include? pass } 602 raise_error "IR file not found for pass: #{pass}. Possible cause: you forgot to select METHOD first" unless @current_file_index 603 @ir_scope = SearchScope.from_file(@ir_files[@current_file_index], 'IR') 604 end 605 606 def PASS_AFTER_NEXT(pass) 607 return if @options.release 608 609 $current_pass = "Pass after next: #{pass}" 610 index = @ir_files[(@current_file_index + 1)..-1].index { |x| File.basename(x).include? pass } 611 raise_error "IR file not found for pass: #{pass}. Possible cause: you forgot to select METHOD first" unless index 612 @current_file_index += 1 + index 613 @ir_scope = SearchScope.from_file(@ir_files[@current_file_index], 'IR') 614 end 615 616 def PASS_BEFORE(pass) 617 return if @options.release 618 619 $current_pass = "Pass before: #{pass}" 620 @current_file_index = @ir_files.index { |x| File.basename(x).include? pass } 621 raise_error "IR file not found for pass: #{pass}. Possible cause: you forgot to select METHOD first" unless @current_file_index 622 @ir_scope = SearchScope.from_file(@ir_files[@current_file_index - 1], 'IR') 623 end 624 625 def READ_FILE(filename) 626 path = "#{@cwd}/#{filename}" 627 raise_error "File `#{filename}` not found" unless File.file?(path) 628 @ir_scope = SearchScope.from_file(path, 'Plain text') 629 end 630 631 def run 632 log.info "Running \"#{@name}\"" 633 $checker_name = @name 634 begin 635 self.instance_eval @code 636 rescue SkipException 637 log.info "Skipped: \"#{@name}\"" 638 else 639 log.info "Success: \"#{@name}\"" 640 end 641 clear_data 642 end 643 644 def clear_data 645 if !@options.keep_data 646 FileUtils.rm_rf("#{@cwd}/ir_dump") 647 FileUtils.rm_rf("#{@cwd}/events.csv") 648 FileUtils.rm_rf("#{@cwd}/disasm.txt") 649 FileUtils.rm_rf("#{@cwd}/console.out") 650 end 651 end 652end 653 654def read_checks(options) 655 checks = [] 656 check = nil 657 check_llvm = nil 658 checker_start = "#{options.command_token} CHECKER" 659 disabled_checker_start = "#{options.command_token} DISABLED_CHECKER" 660 File.readlines(options.source).each do |line| 661 if check 662 unless line.start_with? options.command_token 663 check = nil 664 check_llvm = nil 665 next 666 end 667 raise "No space between two checkers: '#{line.strip}'" if line.start_with? checker_start 668 if line.include? "RUN_AOT" 669 checks << check_llvm 670 end 671 check.append_line(line[options.command_token.size..-1]) unless check == :disabled_check 672 check_llvm.append_line(line[options.command_token.size..-1]) unless check == :disabled_check 673 else 674 next unless line.start_with? options.command_token 675 if line.start_with? checker_start 676 name = line.split(' ', 3)[2].strip 677 raise "Checker with name '#{name}'' already exists" if checks.any? { |x| x.name == name } 678 679 check = Checker.new(options, name) 680 check_llvm = Checker.new(options, "#{name} LLVMAOT") 681 check_llvm.set_llvm_paoc() 682 checks << check 683 else 684 raise "Line '#{line.strip}' does not belong to any checker" unless line.start_with? disabled_checker_start 685 check = :disabled_check 686 next 687 end 688 end 689 end 690 checks 691end 692 693def main(options) 694 read_checks(options).each(&:run) 695 0 696end 697 698if __FILE__ == $PROGRAM_NAME 699 main(options) 700end 701 702# Somehow ruby resolves `Checker` name to another class in a Testing scope, so make this global 703# variable to refer to it from unit tests. I believe there is more proper way to do it, but I 704# didn't find it at first glance. 705$CheckerForTest = Checker 706