rubycc 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +61 -0
  3. data/README.md +26 -14
  4. data/data/verified_gems.json +85 -63
  5. data/exe/rubycc-ar +11 -3
  6. data/include/libc/sys/cdefs.h +12 -0
  7. data/lib/rubycc/backend/aarch64.rb +705 -117
  8. data/lib/rubycc/backend/slot_residency.rb +169 -0
  9. data/lib/rubycc/backend/x86_64.rb +924 -137
  10. data/lib/rubycc/command_line.rb +339 -0
  11. data/lib/rubycc/compile_error.rb +6 -3
  12. data/lib/rubycc/compiler.rb +17 -2
  13. data/lib/rubycc/diagnostics.rb +105 -0
  14. data/lib/rubycc/doctor/gemfile.rb +12 -3
  15. data/lib/rubycc/doctor/verified_gems.rb +5 -1
  16. data/lib/rubycc/driver.rb +66 -10
  17. data/lib/rubycc/front/ast.rb +18 -7
  18. data/lib/rubycc/front/constant_evaluator.rb +12 -0
  19. data/lib/rubycc/front/lexeme_reader.rb +3 -1
  20. data/lib/rubycc/front/parser.rb +51 -18
  21. data/lib/rubycc/ir/analysis.rb +82 -0
  22. data/lib/rubycc/ir/call_convention.rb +74 -7
  23. data/lib/rubycc/ir/generator.rb +319 -9
  24. data/lib/rubycc/ir/ir.rb +39 -1
  25. data/lib/rubycc/ir/promotion.rb +255 -0
  26. data/lib/rubycc/ir/simplify.rb +570 -0
  27. data/lib/rubycc/link/library_resolver.rb +17 -5
  28. data/lib/rubycc/link/partial_linker.rb +8 -1
  29. data/lib/rubycc/link/shared_linker.rb +2 -2
  30. data/lib/rubycc/mkmf_shim.rb +178 -12
  31. data/lib/rubycc/objfile/ar_archive.rb +13 -2
  32. data/lib/rubycc/objfile/elf_reader.rb +13 -2
  33. data/lib/rubycc/pkgconf/parser.rb +4 -0
  34. data/lib/rubycc/pkgconf/resolver.rb +3 -1
  35. data/lib/rubycc/pkgconf/system_path_filter.rb +8 -2
  36. data/lib/rubycc/preprocess/preprocessor.rb +161 -37
  37. data/lib/rubycc/preprocess/scanner.rb +69 -14
  38. data/lib/rubycc/preprocess/token_converter.rb +11 -1
  39. data/lib/rubycc/rmake/cli.rb +32 -4
  40. data/lib/rubycc/rmake/executor.rb +157 -226
  41. data/lib/rubycc/rmake/makefile.rb +35 -13
  42. data/lib/rubycc/rmake/parser.rb +8 -2
  43. data/lib/rubycc/rmake/rmake.rb +1 -0
  44. data/lib/rubycc/rmake/tool_command.rb +69 -0
  45. data/lib/rubycc/shell.rb +510 -0
  46. data/lib/rubycc/type.rb +23 -7
  47. data/lib/rubycc/version.rb +1 -1
  48. data/lib/rubycc.rb +12 -0
  49. data/lib/rubygems_plugin.rb +31 -3
  50. metadata +14 -3
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "rbconfig"
4
+ require_relative "command_line"
4
5
 
5
6
  # The mkmf integration shim: requiring this file (before `require "mkmf"`)
6
7
  # reroutes every conftest command mkmf issues — compile, link, preprocess — to
@@ -20,31 +21,195 @@ require "rbconfig"
20
21
  # in-process only (no file on disk is modified) and idempotent: requiring the
21
22
  # shim twice is a no-op.
22
23
  #
23
- # The rubycc executables shebang-launch `require "rubycc"`, which needs the
24
- # repository's `lib/` on the load path when rubycc is run from a checkout rather
25
- # than an installed gem. The shim prepends that directory to `RUBYLIB` so the
26
- # value propagates to the child processes mkmf spawns.
24
+ # The rubycc executables are launched through the running interpreter — the
25
+ # rewritten `CC` is `<RbConfig.ruby> <path>/exe/rubycc`, not the bare path. Their
26
+ # `#!/usr/bin/env ruby` shebang is resolved by the kernel, and a minimal image
27
+ # (DESIGN R5) need not have /usr/bin/env: `dhi.io/ruby:4` has no coreutils at
28
+ # all, and there every exec of the executable fails with status 127 before a line
29
+ # of rubycc runs. Naming the interpreter also settles *which* Ruby compiles —
30
+ # `env ruby` takes the first one on PATH, which on a multi-Ruby host is not
31
+ # necessarily the one the gem is being installed into.
32
+ #
33
+ # They also `require "rubycc"`, which needs the repository's `lib/` on the load
34
+ # path when rubycc is run from a checkout rather than an installed gem. The shim
35
+ # prepends that directory to `RUBYLIB` so the value propagates to the child
36
+ # processes mkmf spawns.
37
+ #
38
+ # Rewriting the RbConfig keys settles *what* mkmf runs; ShellFreeCommands
39
+ # settles *how*. mkmf builds each conftest command as one string and hands it to
40
+ # `system`/`IO.popen`, which lets Ruby decide between exec'ing it and handing it
41
+ # to /bin/sh — and the environment DESIGN R5 targets has no /bin/sh. The shim
42
+ # prepends the two methods that spawn (`xsystem`, `xpopen`) and converts a string
43
+ # command into an argv array first, so nothing about the run depends on a shell
44
+ # being there. A command that genuinely needs shell interpretation is refused
45
+ # with its reason written to mkmf.log rather than being run through a shell that
46
+ # may or may not exist.
27
47
  module Rubycc
28
48
  module MkmfShim
29
49
  # The repository's lib/ directory (this file lives at lib/rubycc/mkmf_shim.rb)
30
50
  # and the gcc-compatible executables that stand in for the external toolchain.
31
51
  LIB_DIR = File.expand_path("..", __dir__)
32
- RUBYCC_EXE = File.expand_path("../../exe/rubycc", __dir__)
33
- PKGCONF_EXE = File.expand_path("../../exe/rubycc-pkgconf", __dir__)
52
+ EXE_DIR = File.expand_path("../../exe", __dir__)
53
+ RUBYCC_EXE = File.join(EXE_DIR, "rubycc")
54
+ PKGCONF_EXE = File.join(EXE_DIR, "rubycc-pkgconf")
55
+
56
+ # The interpreter every rubycc executable is launched through: this very
57
+ # process's Ruby, by absolute path. RbConfig.ruby is what mkmf, RubyGems and
58
+ # rubycc's own doctor already use to re-enter Ruby.
59
+ RUBY = RbConfig.ruby
60
+
61
+ # A command mkmf can embed verbatim, built from +words+: the interpreter
62
+ # first, then the executable (see the file comment — the shebang is not
63
+ # resolvable everywhere). Each word is quoted, because a command that has to
64
+ # travel as a single string comes apart at the first space in an
65
+ # installation path, and both splitters that read it back — rubycc's own and
66
+ # the Shellwords RubyGems uses — honour the same quoting.
67
+ def self.launcher(*words)
68
+ words.map { |word| CommandLine.quote(word) }.join(" ")
69
+ end
70
+
71
+ # The rubycc compiler as mkmf must spell it.
72
+ RUBYCC_COMMAND = launcher(RUBY, RUBYCC_EXE)
34
73
 
35
74
  # The RbConfig keys rewritten to point at rubycc. Each maps to the command
36
- # string mkmf embeds verbatim when it expands `$(CC)` and friends: a single
37
- # shell-metacharacter-free executable path (plus, for the compound tools, the
38
- # driver's own mode flag) so mkmf's `system(env, *command)` execs it directly.
75
+ # string mkmf embeds verbatim when it expands `$(CC)` and friends: shell-
76
+ # metacharacter-free words (the interpreter, the executable and, for the
77
+ # compound tools, the driver's own mode flag) so mkmf's `system(env,
78
+ # *command)` execs them directly.
79
+ #
80
+ # PKG_CONFIG is the exception that stays a bare path: mkmf resolves it with
81
+ # `find_executable0` (mkmf.rb's pkg_config) before running it, and that stats
82
+ # the value as one file name — a two-word command would simply not be found
83
+ # and pkg-config support would silently switch itself off. The interpreter is
84
+ # put in front of it at spawn time instead, by ShellFreeCommands.
39
85
  def self.replacements
40
86
  {
41
- "CC" => RUBYCC_EXE,
42
- "LDSHARED" => "#{RUBYCC_EXE} -shared",
43
- "CPP" => "#{RUBYCC_EXE} -E",
87
+ "CC" => RUBYCC_COMMAND,
88
+ "LDSHARED" => "#{RUBYCC_COMMAND} -shared",
89
+ "CPP" => "#{RUBYCC_COMMAND} -E",
44
90
  "PKG_CONFIG" => PKGCONF_EXE
45
91
  }
46
92
  end
47
93
 
94
+ # Raised when a conftest command cannot be run without a shell. It is a
95
+ # refusal, not a build failure: mkmf reads a false return as "the compiler
96
+ # cannot build this", which would misreport an unrunnable command as a
97
+ # missing header or a broken toolchain, so the shim stops the extconf
98
+ # instead.
99
+ class ShellRequiredError < Rubycc::Error; end
100
+
101
+ # Prepended to MakeMakefile so the two methods that spawn a conftest —
102
+ # `xsystem` (compile/link/run) and `xpopen` (the same, reading the output) —
103
+ # receive an argv array where mkmf built a string. mkmf's own
104
+ # `expand_command` keeps whatever shape it is given (an Array stays an Array
105
+ # and is spawned directly; a String stays a String and reaches the shell),
106
+ # so converting the command on the way in is the whole change: the logging,
107
+ # the werror handling and the env hash are still mkmf's.
108
+ #
109
+ # The conversion runs mkmf's `$(VAR)` expansion *before* splitting, which is
110
+ # the order a shell saw: mkmf expanded the variables and sh did the word
111
+ # splitting. `super` expands again, on words that no longer hold `$(...)`.
112
+ module ShellFreeCommands
113
+ def xsystem(command, werror: false)
114
+ super(rubycc_spawn_form(command), werror: werror)
115
+ end
116
+
117
+ def xpopen(command, *mode, &block)
118
+ super(rubycc_spawn_form(command), *mode, &block)
119
+ end
120
+
121
+ private
122
+
123
+ # The command as something spawnable without a shell. Arrays are already
124
+ # that and only have the interpreter supplied (mkmf builds the pkg-config
125
+ # calls that way); a string is expanded, split and then treated the same.
126
+ # The method name is prefixed because prepending to MakeMakefile puts it on
127
+ # every extconf's Object.
128
+ def rubycc_spawn_form(command)
129
+ return MkmfShim.interpreter_form(command) unless command.is_a?(String)
130
+
131
+ _env, expanded = expand_command(command)
132
+ MkmfShim.spawn_form(expanded)
133
+ end
134
+ end
135
+
136
+ # Split +command+ into the argv array mkmf should spawn, or refuse it.
137
+ # A one-word command comes back as `[[program, program]]`: `system(env, str)`
138
+ # with a single string would put Ruby back in charge of choosing between exec
139
+ # and the shell (it scans for metacharacters and splits on spaces), and the
140
+ # explicit [program, argv0] form takes that choice away — `./conftest` and a
141
+ # path with a space in it are then spawned the same way.
142
+ def self.spawn_form(command)
143
+ argv = interpreter_form(CommandLine.argv(command))
144
+ argv.length == 1 ? [[argv[0], argv[0]]] : argv
145
+ rescue CommandLine::UnsupportedSyntaxError => e
146
+ refuse!(e, command)
147
+ end
148
+
149
+ # +argv+ with the interpreter put in front when the command runs one of
150
+ # rubycc's own executables directly. The rewritten `CC`/`LDSHARED`/`CPP`
151
+ # already carry it, but `PKG_CONFIG` cannot (see .replacements), and mkmf
152
+ # spawns pkg-config as an array — which reaches the kernel verbatim, shebang
153
+ # line and all. Supplying the interpreter here covers both spellings with one
154
+ # rule. A leading env hash (mkmf passes PKG_CONFIG_PATH that way) is skipped
155
+ # over rather than counted as the program word.
156
+ def self.interpreter_form(argv)
157
+ return argv unless argv.is_a?(Array)
158
+
159
+ index = argv.index { |word| !word.is_a?(Hash) }
160
+ return argv if index.nil? || !own_executable?(argv[index])
161
+
162
+ argv.dup.insert(index, RUBY)
163
+ end
164
+
165
+ # Whether +word+ names one of the executables this gem ships — the files
166
+ # whose `#!/usr/bin/env ruby` line an exec would have to resolve.
167
+ def self.own_executable?(word)
168
+ word.is_a?(String) && File.dirname(word) == EXE_DIR
169
+ end
170
+
171
+ # Report a command the shim will not run. The reason goes to mkmf.log, where
172
+ # anyone debugging a failed extension build already looks, and then the
173
+ # exception stops the extconf: a silent false here is exactly the
174
+ # misdiagnosis ("You have to install development tools first") that hid this
175
+ # whole problem before.
176
+ def self.refuse!(error, command)
177
+ message = <<~MSG
178
+ rubycc: refusing to run a conftest command that needs a shell (#{error.construct}):
179
+ #{command}
180
+ The mkmf shim spawns conftest commands as an argv array because the target
181
+ environment has no /bin/sh (DESIGN R5), and it does not fall back to one:
182
+ a fallback would make the result depend on whether a shell happens to be
183
+ installed. Nothing was run.
184
+ MSG
185
+ log_to_mkmf(message)
186
+ raise ShellRequiredError, message
187
+ end
188
+
189
+ # Append +message+ to mkmf.log through mkmf's own Logging module (which
190
+ # opens the file if it is not open yet). Logging is best-effort: if the log
191
+ # cannot be written the refusal itself must still be raised.
192
+ def self.log_to_mkmf(message)
193
+ return unless defined?(::MakeMakefile::Logging)
194
+
195
+ ::MakeMakefile::Logging.message("%s", message)
196
+ rescue SystemCallError, IOError
197
+ nil
198
+ end
199
+
200
+ # Put ShellFreeCommands in front of MakeMakefile's own methods.
201
+ #
202
+ # mkmf is normally *not* loaded yet at this point: the shim is injected with
203
+ # `RUBYOPT=-rrubycc/mkmf_shim`, which runs before extconf.rb requires mkmf.
204
+ # Defining the module first and prepending to it works in that order because
205
+ # mkmf reopens the module rather than replacing it, and its closing
206
+ # `include MakeMakefile` carries the prepended module into Object with it.
207
+ # Prepending an already-prepended module is a no-op, so this is idempotent.
208
+ def self.prepend_shell_free_commands!
209
+ Object.const_set(:MakeMakefile, Module.new) unless defined?(::MakeMakefile)
210
+ ::MakeMakefile.prepend(ShellFreeCommands)
211
+ end
212
+
48
213
  # Rewrites the toolchain keys in both RbConfig hashes and prepends the
49
214
  # repository lib/ to RUBYLIB. Idempotent: the RUBYLIB entry is added only
50
215
  # once, and re-assigning the same command strings is harmless.
@@ -65,6 +230,7 @@ module Rubycc
65
230
  end
66
231
 
67
232
  prepend_rubylib!
233
+ prepend_shell_free_commands!
68
234
  end
69
235
 
70
236
  # Puts the repository lib/ at the front of RUBYLIB so a rubycc executable
@@ -193,6 +193,14 @@ module Rubycc
193
193
 
194
194
  # Resolves a `/N` reference: read from byte `offset` in the `//` table up to
195
195
  # the terminating `\n` and drop the trailing `/` GNU appends to each entry.
196
+ #
197
+ # The slice is left as bytes, like every other name this reader hands back
198
+ # (lib/rubycc.rb). An archive names a member in whatever bytes the
199
+ # filesystem gave it, and two strings holding the same non-ASCII bytes
200
+ # under different encodings are neither == nor eql? and hash apart — so
201
+ # tagging a name here would make it differ from a SHORT name spelled with
202
+ # the same bytes, and whether a caller's comparison hit would then depend
203
+ # on how long the name happened to be.
196
204
  def resolve_long_name(offset, pos)
197
205
  table = @name_table or
198
206
  raise ArFormatError, "member at offset #{pos} references a name table that is absent"
@@ -201,7 +209,7 @@ module Rubycc
201
209
  end
202
210
 
203
211
  stop = table.index("\n".b, offset) || table.bytesize
204
- table.byteslice(offset...stop).chomp("/").force_encoding(Encoding::UTF_8)
212
+ table.byteslice(offset...stop).chomp("/")
205
213
  end
206
214
 
207
215
  # Turns the raw `/` symbol table into name -> member entries. Deferred until
@@ -223,7 +231,10 @@ module Rubycc
223
231
  cursor = 0
224
232
  count.times do |i|
225
233
  stop = names_blob.index("\0".b, cursor) or break
226
- name = names_blob.byteslice(cursor...stop).force_encoding(Encoding::UTF_8)
234
+ # Bytes, like the member names: a symbol name is whatever bytes the
235
+ # member's ELF string table held, and one spelling for every name a
236
+ # reader hands back is the whole point (see resolve_long_name).
237
+ name = names_blob.byteslice(cursor...stop)
227
238
  cursor = stop + 1
228
239
  member = by_offset[offsets[i]]
229
240
  @symbols << { name: name, member: member } if member
@@ -627,11 +627,22 @@ module Rubycc
627
627
  # A NUL-terminated string starting at `offset` within a string table's
628
628
  # bytes; an out-of-range offset yields "" rather than raising, since a
629
629
  # zero sh_name legitimately points at the leading NUL (the empty name).
630
+ #
631
+ # The slice is left as bytes, like everything rubycc reads (lib/rubycc.rb),
632
+ # and this one method is where every name the reader hands back comes from:
633
+ # section names, .symtab / .dynsym symbol names, DT_SONAME / DT_NEEDED, and
634
+ # version names. A string table holds whatever bytes produced the object —
635
+ # a C identifier cannot carry non-ASCII, an assembler label can — and two
636
+ # strings holding the same non-ASCII bytes under different encodings are
637
+ # neither == nor eql? and hash apart, so tagging here would leave a name
638
+ # unfindable through the bytes a caller already holds (#symbol, #section)
639
+ # and spelled differently from the same name read by ArReader, which takes
640
+ # it out of this very string table when it builds an archive's index.
630
641
  def read_string(strtab, offset)
631
- return "" if offset >= strtab.bytesize
642
+ return "".b if offset >= strtab.bytesize
632
643
 
633
644
  stop = strtab.index("\0".b, offset) || strtab.bytesize
634
- strtab.byteslice(offset...stop).force_encoding(Encoding::UTF_8)
645
+ strtab.byteslice(offset...stop)
635
646
  end
636
647
 
637
648
  # Guards every structural read: the whole [offset, length) span must lie
@@ -27,7 +27,11 @@ module Rubycc
27
27
  new(text, path: path).parse
28
28
  end
29
29
 
30
+ # Bytes in (lib/rubycc.rb): the .pc grammar is spelled in ASCII, and the
31
+ # field values are carried through (a US-ASCII-tagged Description makes
32
+ # String#strip raise).
30
33
  def initialize(text, path: nil)
34
+ text = text.b unless text.encoding == Encoding::BINARY
31
35
  @text = text
32
36
  @path = path
33
37
  @variables = {}
@@ -34,7 +34,9 @@ module Rubycc
34
34
  path = SearchPath.find(name, directories: @directories)
35
35
  raise NotFoundError, "package '#{name}' not found (searched #{@directories.join(File::PATH_SEPARATOR)})" unless path
36
36
 
37
- @cache[name] = Parser.parse(File.read(path), path: path)
37
+ # Bytes: a .pc file's Name/Description routinely carry non-ASCII (a
38
+ # copyright sign, an accented maintainer name).
39
+ @cache[name] = Parser.parse(File.binread(path), path: path)
38
40
  end
39
41
 
40
42
  # Own Cflags, then each public Requires recursively, then each
@@ -108,9 +108,15 @@ module Rubycc
108
108
  # An explicit value replaces the default outright, even when it is empty
109
109
  # (`PKG_CONFIG_SYSTEM_INCLUDE_PATH=` then means "no directory is a
110
110
  # system directory"); only an unset variable falls back to +defaults+.
111
+ #
112
+ # The list is held as bytes (lib/rubycc.rb): +value+ comes from the
113
+ # environment, while the directories it is compared against in
114
+ # #reject_system come out of a .pc file, and for a non-ASCII directory a
115
+ # differing encoding never compares equal — the `-I`/`-L` the environment
116
+ # asked to suppress would be emitted instead.
111
117
  def directory_list(value, defaults)
112
- list = value.nil? ? defaults : value.split(File::PATH_SEPARATOR).reject(&:empty?)
113
- list.map { |dir| normalize(dir) }
118
+ list = value.nil? ? defaults : value.b.split(File::PATH_SEPARATOR).reject(&:empty?)
119
+ list.map { |dir| normalize(dir.b) }
114
120
  end
115
121
 
116
122
  def set?(value)