mirror of
https://github.com/ruby/ruby.git
synced 2022-11-09 12:17:21 -05:00
1c67c46efb
* tool/transform_mjit_header.rb (MJITHeader.conflicting_types?): Add workaround for Solaris 10 with old GCC (4.6.2), that is essentially the same as for AIX (commit r62326), but probably due to different GCC versions, different error message is shown. [Bug #14751] [ruby-dev:50541] git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@63400 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
277 lines
9 KiB
Ruby
277 lines
9 KiB
Ruby
# Copyright (C) 2017 Vladimir Makarov, <vmakarov@redhat.com>
|
|
# This is a script to transform functions to static inline.
|
|
# Usage: transform_mjit_header.rb <c-compiler> <header file> <out>
|
|
|
|
require 'fileutils'
|
|
require 'tempfile'
|
|
|
|
PROGRAM = File.basename($0, ".*")
|
|
|
|
module MJITHeader
|
|
ATTR_VALUE_REGEXP = /[^()]|\([^()]*\)/
|
|
ATTR_REGEXP = /__attribute__\s*\(\(#{ATTR_VALUE_REGEXP}*\)\)/
|
|
FUNC_HEADER_REGEXP = /\A(\s*#{ATTR_REGEXP})*[^\[{(]*\((#{ATTR_REGEXP}|[^()])*\)(\s*#{ATTR_REGEXP})*\s*/
|
|
TARGET_NAME_REGEXP = /\A(rb|ruby|vm|insn|attr)_/
|
|
|
|
# Predefined macros for compilers which are already supported by MJIT.
|
|
# We're going to support cl.exe too (WIP) but `cl.exe -E` can't produce macro.
|
|
SUPPORTED_CC_MACROS = [
|
|
'__GNUC__', # gcc
|
|
'__clang__', # clang
|
|
]
|
|
|
|
# These macros are relied on this script's transformation
|
|
PREFIXED_MACROS = [
|
|
'ALWAYS_INLINE',
|
|
]
|
|
|
|
# For MinGW's ras.h. Those macros have its name in its definition and can't be preprocessed multiple times.
|
|
RECURSIVE_MACROS = %w[
|
|
RASCTRYINFO
|
|
RASIPADDR
|
|
]
|
|
|
|
IGNORED_FUNCTIONS = [
|
|
'vm_search_method_slowpath', # This increases the time to compile when inlined. So we use it as external function.
|
|
'rb_equal_opt', # Not used from VM and not compilable
|
|
]
|
|
|
|
ALWAYS_INLINED_FUNCTIONS = [
|
|
'vm_opt_plus',
|
|
'vm_opt_minus',
|
|
'vm_opt_mult',
|
|
'vm_opt_div',
|
|
'vm_opt_mod',
|
|
'vm_opt_neq',
|
|
'vm_opt_lt',
|
|
'vm_opt_le',
|
|
'vm_opt_gt',
|
|
'vm_opt_ge',
|
|
'vm_opt_ltlt',
|
|
'vm_opt_aref',
|
|
'vm_opt_aset',
|
|
'vm_opt_aref_with',
|
|
'vm_opt_aset_with',
|
|
'vm_opt_not',
|
|
'vm_getinstancevariable',
|
|
'vm_setinstancevariable',
|
|
'vm_setivar',
|
|
]
|
|
|
|
# Return start..stop of last decl in CODE ending STOP
|
|
def self.find_decl(code, stop)
|
|
level = 0
|
|
i = stop
|
|
while i = code.rindex(/[;{}]/, i)
|
|
if level == 0 && stop != i && decl_found?($&, i)
|
|
return decl_start($&, i)..stop
|
|
end
|
|
case $&
|
|
when '}'
|
|
level += 1
|
|
when '{'
|
|
level -= 1
|
|
end
|
|
i -= 1
|
|
end
|
|
nil
|
|
end
|
|
|
|
def self.decl_found?(code, i)
|
|
i == 0 || code == ';' || code == '}'
|
|
end
|
|
|
|
def self.decl_start(code, i)
|
|
if i == 0 && code != ';' && code != '}'
|
|
0
|
|
else
|
|
i + 1
|
|
end
|
|
end
|
|
|
|
# Given DECL return the name of it, nil if failed
|
|
def self.decl_name_of(decl)
|
|
ident_regex = /\w+/
|
|
decl = decl.gsub(/^#.+$/, '') # remove macros
|
|
reduced_decl = decl.gsub(ATTR_REGEXP, '') # remove attributes
|
|
su1_regex = /{[^{}]*}/
|
|
su2_regex = /{([^{}]|#{su1_regex})*}/
|
|
su3_regex = /{([^{}]|#{su2_regex})*}/ # 3 nested structs/unions is probably enough
|
|
reduced_decl.gsub!(su3_regex, '') # remove structs/unions in the header
|
|
id_seq_regex = /\s*(#{ident_regex}(\s+|\s*[*]+\s*))*/
|
|
# Process function header:
|
|
match = /\A#{id_seq_regex}(?<name>#{ident_regex})\s*\(/.match(reduced_decl)
|
|
return match[:name] if match
|
|
# Process non-function declaration:
|
|
reduced_decl.gsub!(/\s*=[^;]+(?=;)/, '') # remove initialization
|
|
match = /#{id_seq_regex}(?<name>#{ident_regex})/.match(reduced_decl);
|
|
return match[:name] if match
|
|
nil
|
|
end
|
|
|
|
# Return true if CC with CFLAGS compiles successfully the current code.
|
|
# Use STAGE in the message in case of a compilation failure
|
|
def self.check_code!(code, cc, cflags, stage)
|
|
with_code(code) do |path|
|
|
cmd = "#{cc} #{cflags} #{path}"
|
|
out = IO.popen(cmd, err: [:child, :out], &:read)
|
|
unless $?.success?
|
|
STDERR.puts "error in #{stage} header file:\n#{out}"
|
|
exit false
|
|
end
|
|
end
|
|
end
|
|
|
|
# Remove unpreprocessable macros
|
|
def self.remove_harmful_macros!(code)
|
|
code.gsub!(/^#define #{Regexp.union(RECURSIVE_MACROS)} .*$/, '')
|
|
end
|
|
|
|
# -dD outputs those macros, and it produces redefinition warnings or errors
|
|
# This assumes common.mk passes `-DMJIT_HEADER` first when it creates rb_mjit_header.h.
|
|
def self.remove_predefined_macros!(code)
|
|
code.sub!(/\A(#define [^\n]+|\n)*(#define MJIT_HEADER 1\n)/, '\2')
|
|
end
|
|
|
|
# Return [macro, others]. But others include PREFIXED_MACROS to be used in code.
|
|
def self.separate_macro_and_code(code)
|
|
code.lines.partition do |l|
|
|
l.start_with?('#') && PREFIXED_MACROS.all? { |m| !l.start_with?("#define #{m}") }
|
|
end.map! { |lines| lines.join('') }
|
|
end
|
|
|
|
def self.write(code, out)
|
|
FileUtils.mkdir_p(File.dirname(out))
|
|
File.binwrite("#{out}.new", code)
|
|
FileUtils.mv("#{out}.new", out)
|
|
end
|
|
|
|
# Note that this checks runruby. This conservatively covers platform names.
|
|
def self.windows?
|
|
RUBY_PLATFORM =~ /mswin|mingw|msys/
|
|
end
|
|
|
|
def self.cl_exe?(cc)
|
|
cc =~ /\Acl(\z| |\.exe)/
|
|
end
|
|
|
|
# If code has macro which only supported compilers predefine, return true.
|
|
def self.supported_header?(code)
|
|
SUPPORTED_CC_MACROS.any? { |macro| code =~ /^#\s*define\s+#{Regexp.escape(macro)}\b/ }
|
|
end
|
|
|
|
# This checks if syntax check outputs one of the following messages.
|
|
# "error: conflicting types for 'restrict'"
|
|
# "error: redefinition of parameter 'restrict'"
|
|
# If it's true, this script regards platform as AIX or Solaris and adds -std=c99 as workaround.
|
|
def self.conflicting_types?(code, cc, cflags)
|
|
with_code(code) do |path|
|
|
cmd = "#{cc} #{cflags} #{path}"
|
|
out = IO.popen(cmd, err: [:child, :out], &:read)
|
|
!$?.success? &&
|
|
(out.match?(/error: conflicting types for '[^']+'/) ||
|
|
out.match?(/error: redefinition of parameter '[^']+'/))
|
|
end
|
|
end
|
|
|
|
def self.with_code(code)
|
|
Tempfile.open(['', '.c'], mode: File::BINARY) do |f|
|
|
f.puts code
|
|
f.close
|
|
return yield(f.path)
|
|
end
|
|
end
|
|
private_class_method :with_code
|
|
end
|
|
|
|
if ARGV.size != 3
|
|
abort "Usage: #{$0} <c-compiler> <header file> <out>"
|
|
end
|
|
|
|
cc = ARGV[0]
|
|
code = File.binread(ARGV[1]) # Current version of the header file.
|
|
outfile = ARGV[2]
|
|
if MJITHeader.cl_exe?(cc)
|
|
cflags = '-DMJIT_HEADER -Zs'
|
|
else
|
|
cflags = '-S -DMJIT_HEADER -fsyntax-only -Werror=implicit-function-declaration -Werror=implicit-int -Wfatal-errors'
|
|
end
|
|
|
|
if !MJITHeader.cl_exe?(cc) && !MJITHeader.supported_header?(code)
|
|
puts "This compiler (#{cc}) looks not supported for MJIT. Giving up to generate MJIT header."
|
|
MJITHeader.write("#error MJIT does not support '#{cc}' yet", outfile)
|
|
exit
|
|
end
|
|
|
|
MJITHeader.remove_predefined_macros!(code)
|
|
|
|
if MJITHeader.windows? # transformation is broken with Windows headers for now
|
|
MJITHeader.remove_harmful_macros!(code)
|
|
MJITHeader.check_code!(code, cc, cflags, 'initial')
|
|
puts "\nSkipped transforming external functions to static on Windows."
|
|
MJITHeader.write(code, outfile)
|
|
exit
|
|
end
|
|
|
|
macro, code = MJITHeader.separate_macro_and_code(code) # note: this does not work on MinGW
|
|
code_to_check = "#{code}#{macro}" # macro should not affect code again
|
|
|
|
if MJITHeader.conflicting_types?(code_to_check, cc, cflags)
|
|
cflags = "#{cflags} -std=c99" # For AIX gcc
|
|
end
|
|
|
|
# Check initial file correctness in the manner of final output.
|
|
MJITHeader.check_code!(code_to_check, cc, cflags, 'initial')
|
|
puts "\nTransforming external functions to static:"
|
|
|
|
stop_pos = -1
|
|
extern_names = []
|
|
|
|
# This loop changes function declarations to static inline.
|
|
while (decl_range = MJITHeader.find_decl(code, stop_pos))
|
|
stop_pos = decl_range.begin - 1
|
|
decl = code[decl_range]
|
|
decl_name = MJITHeader.decl_name_of(decl)
|
|
|
|
if MJITHeader::IGNORED_FUNCTIONS.include?(decl_name) && /#{MJITHeader::FUNC_HEADER_REGEXP}{/.match(decl)
|
|
puts "#{PROGRAM}: changing definition of '#{decl_name}' to declaration"
|
|
code[decl_range] = decl.sub(/{.+}/m, ';')
|
|
elsif MJITHeader::ALWAYS_INLINED_FUNCTIONS.include?(decl_name) && match = /#{MJITHeader::FUNC_HEADER_REGEXP}{/.match(decl)
|
|
header = match[0].sub(/{\z/, '').strip
|
|
header = "static inline #{header.sub(/\A((static|inline) )+/, '')}"
|
|
decl[match.begin(0)...match.end(0)] = '{' # remove header
|
|
code[decl_range] = "\nALWAYS_INLINE(#{header});\n#{header} #{decl}"
|
|
elsif extern_names.include?(decl_name) && (decl =~ /#{MJITHeader::FUNC_HEADER_REGEXP};/)
|
|
decl.sub!(/(extern|static|inline) /, ' ')
|
|
unless decl_name =~ /\Aattr_\w+_\w+\z/ # skip too-many false-positive warnings in insns_info.inc.
|
|
puts "#{PROGRAM}: making declaration of '#{decl_name}' static inline"
|
|
end
|
|
|
|
code[decl_range] = "static inline #{decl}"
|
|
elsif (match = /#{MJITHeader::FUNC_HEADER_REGEXP}{/.match(decl)) && (header = match[0]) !~ /static/
|
|
unless decl_name.match(MJITHeader::TARGET_NAME_REGEXP)
|
|
puts "#{PROGRAM}: SKIPPED to transform #{decl_name}"
|
|
next
|
|
end
|
|
|
|
extern_names << decl_name
|
|
decl[match.begin(0)...match.end(0)] = ''
|
|
|
|
if decl =~ /\bstatic\b/
|
|
puts "warning: a static decl inside external definition of '#{decl_name}'"
|
|
end
|
|
|
|
header.sub!(/(extern|inline) /, ' ')
|
|
unless decl_name =~ /\Aattr_\w+_\w+\z/ # skip too-many false-positive warnings in insns_info.inc.
|
|
puts "#{PROGRAM}: making external definition of '#{decl_name}' static inline"
|
|
end
|
|
code[decl_range] = "static inline #{header}#{decl}"
|
|
end
|
|
end
|
|
|
|
code << macro
|
|
|
|
# Check the final file correctness
|
|
MJITHeader.check_code!(code, cc, cflags, 'final')
|
|
|
|
MJITHeader.write(code, outfile)
|