rubycc 1.0.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 (159) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +56 -0
  3. data/LICENSE.txt +21 -0
  4. data/NOTICE +52 -0
  5. data/README.md +208 -0
  6. data/data/README.md +117 -0
  7. data/data/r10_corpus_scan.json +4082 -0
  8. data/data/r10_manual_classification.json +5342 -0
  9. data/data/r10_verification_m4a.json +1531 -0
  10. data/data/r10_verification_m4b.json +1818 -0
  11. data/data/r10_verification_m4c.json +1489 -0
  12. data/data/r10_verification_m4d.json +318 -0
  13. data/data/r10_verification_rbs.json +192 -0
  14. data/data/verified_gems.json +397 -0
  15. data/exe/rmake +16 -0
  16. data/exe/rubycc +9 -0
  17. data/exe/rubycc-ar +114 -0
  18. data/exe/rubycc-doctor +14 -0
  19. data/exe/rubycc-pkgconf +8 -0
  20. data/include/float.h +87 -0
  21. data/include/iso646.h +18 -0
  22. data/include/libc/alloca.h +18 -0
  23. data/include/libc/arpa/inet.h +61 -0
  24. data/include/libc/assert.h +43 -0
  25. data/include/libc/dirent.h +72 -0
  26. data/include/libc/dlfcn.h +58 -0
  27. data/include/libc/features.h +205 -0
  28. data/include/libc/glibc/aarch64/ctype.h +108 -0
  29. data/include/libc/glibc/aarch64/endian.h +54 -0
  30. data/include/libc/glibc/aarch64/errno.h +154 -0
  31. data/include/libc/glibc/aarch64/fcntl.h +144 -0
  32. data/include/libc/glibc/aarch64/inttypes.h +182 -0
  33. data/include/libc/glibc/aarch64/limits.h +63 -0
  34. data/include/libc/glibc/aarch64/pthread.h +131 -0
  35. data/include/libc/glibc/aarch64/setjmp.h +84 -0
  36. data/include/libc/glibc/aarch64/stdint.h +174 -0
  37. data/include/libc/glibc/aarch64/sys/epoll.h +88 -0
  38. data/include/libc/glibc/aarch64/sys/fcntl.h +19 -0
  39. data/include/libc/glibc/aarch64/sys/select.h +75 -0
  40. data/include/libc/glibc/aarch64/sys/stat.h +136 -0
  41. data/include/libc/glibc/aarch64/sys/syscall.h +167 -0
  42. data/include/libc/glibc/aarch64/sys/time.h +74 -0
  43. data/include/libc/glibc/aarch64/sys/types.h +127 -0
  44. data/include/libc/glibc/aarch64/time.h +113 -0
  45. data/include/libc/glibc/x86_64/ctype.h +108 -0
  46. data/include/libc/glibc/x86_64/endian.h +54 -0
  47. data/include/libc/glibc/x86_64/errno.h +154 -0
  48. data/include/libc/glibc/x86_64/fcntl.h +134 -0
  49. data/include/libc/glibc/x86_64/inttypes.h +182 -0
  50. data/include/libc/glibc/x86_64/limits.h +61 -0
  51. data/include/libc/glibc/x86_64/pthread.h +111 -0
  52. data/include/libc/glibc/x86_64/setjmp.h +82 -0
  53. data/include/libc/glibc/x86_64/stdint.h +163 -0
  54. data/include/libc/glibc/x86_64/sys/epoll.h +91 -0
  55. data/include/libc/glibc/x86_64/sys/fcntl.h +19 -0
  56. data/include/libc/glibc/x86_64/sys/select.h +75 -0
  57. data/include/libc/glibc/x86_64/sys/stat.h +130 -0
  58. data/include/libc/glibc/x86_64/sys/syscall.h +188 -0
  59. data/include/libc/glibc/x86_64/sys/time.h +74 -0
  60. data/include/libc/glibc/x86_64/sys/types.h +123 -0
  61. data/include/libc/glibc/x86_64/time.h +113 -0
  62. data/include/libc/grp.h +50 -0
  63. data/include/libc/langinfo.h +126 -0
  64. data/include/libc/link.h +18 -0
  65. data/include/libc/locale.h +83 -0
  66. data/include/libc/math.h +191 -0
  67. data/include/libc/netinet/in.h +126 -0
  68. data/include/libc/netinet/tcp.h +44 -0
  69. data/include/libc/poll.h +46 -0
  70. data/include/libc/pwd.h +58 -0
  71. data/include/libc/regex.h +53 -0
  72. data/include/libc/sched.h +35 -0
  73. data/include/libc/signal.h +204 -0
  74. data/include/libc/stdio.h +157 -0
  75. data/include/libc/stdlib.h +92 -0
  76. data/include/libc/string.h +74 -0
  77. data/include/libc/strings.h +25 -0
  78. data/include/libc/sys/cdefs.h +126 -0
  79. data/include/libc/sys/inotify.h +111 -0
  80. data/include/libc/sys/ioctl.h +36 -0
  81. data/include/libc/sys/mman.h +65 -0
  82. data/include/libc/sys/param.h +41 -0
  83. data/include/libc/sys/resource.h +109 -0
  84. data/include/libc/sys/socket.h +167 -0
  85. data/include/libc/sys/statfs.h +82 -0
  86. data/include/libc/sys/timerfd.h +55 -0
  87. data/include/libc/sys/uio.h +40 -0
  88. data/include/libc/sys/un.h +25 -0
  89. data/include/libc/sys/utsname.h +35 -0
  90. data/include/libc/sys/wait.h +135 -0
  91. data/include/libc/termios.h +179 -0
  92. data/include/libc/unistd.h +194 -0
  93. data/include/stdalign.h +16 -0
  94. data/include/stdarg.h +31 -0
  95. data/include/stdatomic.h +158 -0
  96. data/include/stdbool.h +15 -0
  97. data/include/stdckdint.h +28 -0
  98. data/include/stddef.h +60 -0
  99. data/include/stdnoreturn.h +18 -0
  100. data/include/x86intrin.h +16 -0
  101. data/lib/rubycc/backend/aarch64.rb +1724 -0
  102. data/lib/rubycc/backend/x86_64.rb +1369 -0
  103. data/lib/rubycc/compile_error.rb +36 -0
  104. data/lib/rubycc/compiler.rb +305 -0
  105. data/lib/rubycc/doctor/builder.rb +151 -0
  106. data/lib/rubycc/doctor/cli.rb +190 -0
  107. data/lib/rubycc/doctor/fetcher.rb +84 -0
  108. data/lib/rubycc/doctor/gemfile.rb +115 -0
  109. data/lib/rubycc/doctor/verified_gems.rb +114 -0
  110. data/lib/rubycc/doctor.rb +16 -0
  111. data/lib/rubycc/driver.rb +463 -0
  112. data/lib/rubycc/front/ast.rb +528 -0
  113. data/lib/rubycc/front/constant_evaluator.rb +631 -0
  114. data/lib/rubycc/front/initializer_resolver.rb +592 -0
  115. data/lib/rubycc/front/lexeme_reader.rb +460 -0
  116. data/lib/rubycc/front/lexer.rb +232 -0
  117. data/lib/rubycc/front/parser.rb +4122 -0
  118. data/lib/rubycc/front/token.rb +49 -0
  119. data/lib/rubycc/ir/call_convention.rb +486 -0
  120. data/lib/rubycc/ir/generator.rb +6036 -0
  121. data/lib/rubycc/ir/ir.rb +417 -0
  122. data/lib/rubycc/link/compat_runtime.rb +89 -0
  123. data/lib/rubycc/link/errors.rb +13 -0
  124. data/lib/rubycc/link/executable_linker.rb +405 -0
  125. data/lib/rubycc/link/library_resolver.rb +437 -0
  126. data/lib/rubycc/link/partial_linker.rb +546 -0
  127. data/lib/rubycc/link/shared_linker.rb +1732 -0
  128. data/lib/rubycc/mkmf_shim.rb +83 -0
  129. data/lib/rubycc/objfile/ar_archive.rb +437 -0
  130. data/lib/rubycc/objfile/elf_reader.rb +646 -0
  131. data/lib/rubycc/objfile/elf_writer.rb +891 -0
  132. data/lib/rubycc/objfile/relocatable_writer.rb +376 -0
  133. data/lib/rubycc/pkgconf/cli.rb +139 -0
  134. data/lib/rubycc/pkgconf/errors.rb +39 -0
  135. data/lib/rubycc/pkgconf/model.rb +28 -0
  136. data/lib/rubycc/pkgconf/parser.rb +109 -0
  137. data/lib/rubycc/pkgconf/pkgconf.rb +15 -0
  138. data/lib/rubycc/pkgconf/resolver.rb +70 -0
  139. data/lib/rubycc/pkgconf/search_path.rb +38 -0
  140. data/lib/rubycc/pkgconf/system_path_filter.rb +131 -0
  141. data/lib/rubycc/preprocess/constant_expression.rb +219 -0
  142. data/lib/rubycc/preprocess/glibc_version.rb +124 -0
  143. data/lib/rubycc/preprocess/pp_token.rb +62 -0
  144. data/lib/rubycc/preprocess/preprocessor.rb +2020 -0
  145. data/lib/rubycc/preprocess/scanner.rb +290 -0
  146. data/lib/rubycc/preprocess/token_converter.rb +157 -0
  147. data/lib/rubycc/rmake/cli.rb +143 -0
  148. data/lib/rubycc/rmake/errors.rb +71 -0
  149. data/lib/rubycc/rmake/executor.rb +818 -0
  150. data/lib/rubycc/rmake/expander.rb +251 -0
  151. data/lib/rubycc/rmake/makefile.rb +352 -0
  152. data/lib/rubycc/rmake/model.rb +137 -0
  153. data/lib/rubycc/rmake/parser.rb +226 -0
  154. data/lib/rubycc/rmake/rmake.rb +15 -0
  155. data/lib/rubycc/type.rb +1236 -0
  156. data/lib/rubycc/version.rb +5 -0
  157. data/lib/rubycc.rb +36 -0
  158. data/lib/rubygems_plugin.rb +102 -0
  159. metadata +219 -0
@@ -0,0 +1,463 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rbconfig"
4
+ require_relative "compile_error"
5
+ require_relative "compiler"
6
+ require_relative "preprocess/preprocessor"
7
+ require_relative "link/errors"
8
+ require_relative "link/partial_linker"
9
+ require_relative "link/shared_linker"
10
+ require_relative "link/executable_linker"
11
+ require_relative "link/library_resolver"
12
+ require_relative "link/compat_runtime"
13
+
14
+ module Rubycc
15
+ # The gcc-compatible command-line driver: it reads the same option and input
16
+ # vocabulary a compiler driver is invoked with (`rubycc -c -fPIC -Iinc -o x.o
17
+ # x.c`, `rubycc -shared -o x.so a.o b.o -Llib -lz`, `rubycc -o prog main.c
18
+ # util.c`) and drives the existing compiler and linker components to satisfy
19
+ # it. It owns no compilation or linking logic of its own — it classifies the
20
+ # inputs, picks the output mode gcc's flag precedence dictates, and hands the
21
+ # work to Compiler, SharedLinker, ExecutableLinker and LibraryResolver.
22
+ #
23
+ # Three modes are selected exactly as gcc selects them: `-c` compiles each
24
+ # source to an object without linking; `-shared` links every input into a
25
+ # shared object; the absence of a mode flag links into an executable. A
26
+ # one-shot invocation (`-c` absent, a `.c` given) compiles each source in
27
+ # memory and feeds the resulting object bytes straight to the linker, so no
28
+ # intermediate `.o` is ever written to disk. `-E` runs the preprocessor only.
29
+ #
30
+ # Unknown options are handled the tolerant way a build driver must (R6): a
31
+ # documented-but-unmodelled flag family (`-O*`, `-g*`, `-W*`, most `-f*`, `-m*`,
32
+ # `-std=…`, and friends) is accepted and ignored silently, and any other
33
+ # `-`-prefixed token draws a warning and is ignored, so an environment-specific
34
+ # flag mkmf passes never derails a build. A genuine mistake with a definite
35
+ # meaning — a missing `-o` operand — is still an error.
36
+ class Driver
37
+ PROG = "rubycc"
38
+ # C++ is outside rubycc's supported language scope (DESIGN §3.3). Keep the
39
+ # list in step with the corpus census gate so a C++ file passed directly to
40
+ # the driver is diagnosed instead of falling through as an object input.
41
+ CPP_INPUT_EXTENSIONS = %w[.cpp .cc .cxx .c++ .hpp .hxx .hh].freeze
42
+
43
+ # Options that take a separate operand this driver does not act on. They are
44
+ # recognized only so the operand is not mistaken for an input file; both the
45
+ # option and its argument are skipped. (`-isystem` and friends are handled
46
+ # separately, as header search directories.)
47
+ ARG_CONSUMING_IGNORED = %w[-Xlinker -z -u -T -MF -MT -MQ -include -isysroot
48
+ -aux-info -Xassembler -Xpreprocessor].freeze
49
+
50
+ # Bare flags accepted and ignored: linking/codegen switches whose effect this
51
+ # minimal toolchain does not model but whose presence must not be an error.
52
+ SILENT_IGNORE_EXACT = %w[-pipe -pthread -no-pie -pie -rdynamic -static -s
53
+ -nostdlib -nostartfiles -shared-libgcc -static-libgcc
54
+ -symbolic -fsyntax-only].freeze
55
+
56
+ # A command-line usage error (a missing operand, an unsupported combination):
57
+ # reported with the driver's "error:" prefix and a non-zero exit.
58
+ class UsageError < StandardError; end
59
+
60
+ class << self
61
+ # Runs the driver for `argv` and returns the process exit status (0 on
62
+ # success, 1 on any diagnosed failure). `stdout`/`stderr` are injectable so
63
+ # a test can drive the driver in-process and capture its streams.
64
+ def run(argv, stdout: $stdout, stderr: $stderr)
65
+ new(argv, stdout: stdout, stderr: stderr).run
66
+ end
67
+ end
68
+
69
+ def initialize(argv, stdout: $stdout, stderr: $stderr)
70
+ @argv = argv
71
+ @out = stdout
72
+ @err = stderr
73
+ @inputs = [] # [{ path:, kind: }] in command-line order
74
+ @output = nil
75
+ @mode_flag = nil # :compile / :shared / :preprocess (nil => executable)
76
+ @include_paths = []
77
+ @defines = [] # ordered [:define, "NAME[=VAL]"] / [:undef, "NAME"]
78
+ @libraries = [] # -l request strings, in order
79
+ @lib_dirs = [] # -L directories, in order
80
+ @pic = false
81
+ @default_visibility = :default
82
+ @soname = nil
83
+ @system_includes = true # -nostdinc clears this
84
+ @default_libs = true # -nodefaultlibs clears this
85
+ @target = nil # -target/--target; defaults to the host CPU
86
+ end
87
+
88
+ def run
89
+ return print_version if version_requested?
90
+
91
+ parse
92
+ dispatch
93
+ 0
94
+ rescue UsageError => e
95
+ @err.puts "#{PROG}: error: #{e.message}"
96
+ 1
97
+ rescue CompileError => e
98
+ # A compiler diagnostic already carries its gcc-style file:line:col header
99
+ # and caret, so it is printed verbatim.
100
+ @err.puts e.message
101
+ 1
102
+ rescue Backend::UnsupportedError => e
103
+ # A construct the selected backend cannot lower yet: valid C the target is
104
+ # not ready for, reported as a diagnostic rather than a Ruby crash.
105
+ @err.puts "#{PROG}: error: #{e.message}"
106
+ 1
107
+ rescue Link::LinkError => e
108
+ @err.puts "#{PROG}: error: #{e.message}"
109
+ 1
110
+ rescue Errno::ENOENT => e
111
+ @err.puts "#{PROG}: error: #{e.message}"
112
+ 1
113
+ end
114
+
115
+ private
116
+
117
+ # --version / -v keep their existing meaning (print the version and exit),
118
+ # taking precedence over any other argument.
119
+ def version_requested?
120
+ @argv.include?("--version") || @argv.include?("-v")
121
+ end
122
+
123
+ def print_version
124
+ @out.puts "rubycc #{Rubycc::VERSION}"
125
+ 0
126
+ end
127
+
128
+ # --- argument parsing --------------------------------------------------
129
+
130
+ def parse
131
+ i = 0
132
+ i = handle_arg(@argv[i], i) while i < @argv.length
133
+ end
134
+
135
+ # Dispatches one argument and returns the index of the next unconsumed one.
136
+ # The separated ("-I dir") and joined ("-Idir") spellings of the path/macro/
137
+ # library options are both accepted, matching gcc.
138
+ def handle_arg(arg, i)
139
+ case arg
140
+ when "-c" then @mode_flag = :compile; i + 1
141
+ when "-shared" then @mode_flag = :shared; i + 1
142
+ when "-E" then @mode_flag = :preprocess; i + 1
143
+ when "-fPIC", "-fpic", "-fPIE", "-fpie" then @pic = true; i + 1
144
+ when "-fvisibility=hidden" then @default_visibility = :hidden; i + 1
145
+ when "-fvisibility=default" then @default_visibility = :default; i + 1
146
+ when "-fvisibility=internal" then @default_visibility = :internal; i + 1
147
+ when "-fvisibility=protected" then @default_visibility = :protected; i + 1
148
+ when "-nostdinc" then @system_includes = false; i + 1
149
+ when "-nodefaultlibs" then @default_libs = false; i + 1
150
+ when "-target", "--target" then @target = normalize_target(value(arg, i)); i + 2
151
+ when /\A--?target=(.+)\z/m then @target = normalize_target(Regexp.last_match(1)); i + 1
152
+ when "-o" then @output = value(arg, i); i + 2
153
+ when "-I" then @include_paths << value(arg, i); i + 2
154
+ when /\A-I(.+)\z/m then @include_paths << Regexp.last_match(1); i + 1
155
+ when "-isystem", "-iquote", "-idirafter"
156
+ @include_paths << value(arg, i); i + 2
157
+ when "-D" then @defines << [:define, value(arg, i)]; i + 2
158
+ when /\A-D(.+)\z/m then @defines << [:define, Regexp.last_match(1)]; i + 1
159
+ when "-U" then @defines << [:undef, value(arg, i)]; i + 2
160
+ when /\A-U(.+)\z/m then @defines << [:undef, Regexp.last_match(1)]; i + 1
161
+ when "-l" then @libraries << value(arg, i); i + 2
162
+ when /\A-l(.+)\z/m then @libraries << Regexp.last_match(1); i + 1
163
+ when "-L" then @lib_dirs << value(arg, i); i + 2
164
+ when /\A-L(.+)\z/m then @lib_dirs << Regexp.last_match(1); i + 1
165
+ when /\A-Wl,(.+)\z/m then parse_linker_options(Regexp.last_match(1)); i + 1
166
+ when *ARG_CONSUMING_IGNORED
167
+ @argv[i + 1].nil? ? i + 1 : i + 2
168
+ else
169
+ handle_other(arg, i)
170
+ end
171
+ end
172
+
173
+ # A token matching none of the explicit options: an input file, a silently
174
+ # accepted codegen/warning flag, or an unknown option (warned and dropped).
175
+ def handle_other(arg, i)
176
+ if !arg.start_with?("-")
177
+ classify_input(arg)
178
+ elsif silently_ignored?(arg)
179
+ # Accepted with no effect (an optimization/debug/warning switch).
180
+ else
181
+ warning("unknown option '#{arg}' ignored")
182
+ end
183
+ i + 1
184
+ end
185
+
186
+ # Whether `arg` is a documented gcc flag family this toolchain does not model
187
+ # but accepts without complaint: the optimization (`-O*`), debug (`-g*`),
188
+ # warning (`-W*`) and remaining code-generation (`-f*`) switches, the language-standard
189
+ # selector (`-std=…`), and the fixed bare set above. A machine switch (`-m*`)
190
+ # is deliberately excluded — it names a target capability this toolchain does
191
+ # not honor, so it is warned about like any other unmodelled option.
192
+ def silently_ignored?(arg)
193
+ SILENT_IGNORE_EXACT.include?(arg) ||
194
+ arg.match?(/\A-(?:O|g|W|f)/) || arg.start_with?("-std=")
195
+ end
196
+
197
+ # The operand of an option that takes a separate value, erroring with a
198
+ # gcc-style message when it is missing.
199
+ def value(name, i)
200
+ operand = @argv[i + 1]
201
+ raise UsageError, "missing #{value_kind(name)} after '#{name}'" if operand.nil?
202
+
203
+ operand
204
+ end
205
+
206
+ def value_kind(name)
207
+ case name
208
+ when "-o" then "filename"
209
+ when "-I", "-L", "-isystem", "-iquote", "-idirafter" then "directory"
210
+ else "argument"
211
+ end
212
+ end
213
+
214
+ # Interprets the comma-separated linker options in a `-Wl,` passthrough. Only
215
+ # the ones with a driver-level effect are acted on: `-soname NAME` (in the
216
+ # `-soname NAME`, `-soname=NAME` and `-h NAME` spellings) sets the shared
217
+ # object's DT_SONAME. Options that carry a value we ignore (`-rpath` and kin)
218
+ # skip that value so it is not read as a bare option; every other linker
219
+ # option is silently ignored.
220
+ def parse_linker_options(joined)
221
+ tokens = joined.split(",")
222
+ j = 0
223
+ while j < tokens.length
224
+ token = tokens[j]
225
+ case token
226
+ when "-soname", "--soname", "-h" then @soname = tokens[j + 1]; j += 2
227
+ when /\A(?:-soname|--soname|-h)=(.+)\z/ then @soname = Regexp.last_match(1); j += 1
228
+ when "-rpath", "-rpath-link", "--dynamic-linker", "--version-script" then j += 2
229
+ else j += 1
230
+ end
231
+ end
232
+ end
233
+
234
+ # Classifies an input by extension into the role it plays: a `.c` source to
235
+ # compile, a `.o`/`.obj` object or `.a` archive to link, a `.so[.N]` shared
236
+ # library to depend on. C++ source/header suffixes are rejected explicitly;
237
+ # anything else is treated as a linker input object, as gcc defaults an
238
+ # unrecognized suffix to.
239
+ def classify_input(path)
240
+ extension = File.extname(path).downcase
241
+ if CPP_INPUT_EXTENSIONS.include?(extension)
242
+ raise UsageError, "C++ input '#{path}' is not supported"
243
+ end
244
+
245
+ kind =
246
+ case extension
247
+ when ".c" then :source
248
+ when ".o", ".obj" then :object
249
+ when ".a" then :archive
250
+ else path.match?(/\.so(?:\.\d+)*\z/) ? :shared : :object
251
+ end
252
+ @inputs << { path: path, kind: kind }
253
+ end
254
+
255
+ # --- dispatch ----------------------------------------------------------
256
+
257
+ def dispatch
258
+ raise UsageError, "no input file" if @inputs.empty?
259
+
260
+ case mode
261
+ when :preprocess then preprocess_only
262
+ when :compile then ensure_supported_target && compile_only
263
+ when :shared, :executable then ensure_supported_target && link
264
+ end
265
+ end
266
+
267
+ # The code-generation target: the value of -target/--target if given,
268
+ # otherwise the host CPU (RbConfig::CONFIG["host_cpu"], normalized). Both
269
+ # paths run through #normalize_target so a triple like "x86_64-linux-gnu"
270
+ # collapses to its architecture and aliases (amd64) canonicalize.
271
+ def target
272
+ @target ||= normalize_target(RbConfig::CONFIG["host_cpu"])
273
+ end
274
+
275
+ # The C library whose ABI the bundled headers are read under: the host's
276
+ # own (see Preprocessor.host_libc), which is what makes a plain `rubycc`
277
+ # invocation on an Alpine image compile against the musl branches of the
278
+ # bundled headers and the same invocation on a Debian image against the
279
+ # glibc ones. It is not derived from #target: the machine and the libc are
280
+ # independent axes (either arch runs either libc), so it is threaded to the
281
+ # compiler separately rather than read out of Compiler::TARGETS.
282
+ def libc
283
+ @libc ||= Preprocess::Preprocessor.host_libc
284
+ end
285
+
286
+ # Reduces a target spelling — a bare CPU name, a gcc/clang triple, or a
287
+ # host_cpu string — to a canonical architecture name: the part before the
288
+ # first "-" of a triple, with common aliases folded (amd64/x64 -> x86_64,
289
+ # arm64 -> aarch64).
290
+ def normalize_target(spec)
291
+ cpu = spec.to_s.split("-", 2).first.to_s
292
+ case cpu
293
+ when "amd64", "x64" then "x86_64"
294
+ when "arm64" then "aarch64"
295
+ else cpu
296
+ end
297
+ end
298
+
299
+ # Verifies the selected target has an implemented backend, raising a
300
+ # gcc-style usage error for an entirely unknown architecture. x86_64 and
301
+ # aarch64 are both built; anything else is a plain "unsupported target".
302
+ # Returns true so it can gate #dispatch.
303
+ def ensure_supported_target
304
+ return true if Compiler::TARGETS.key?(target)
305
+
306
+ raise UsageError, "unsupported target '#{target}'"
307
+ end
308
+
309
+ # The output mode gcc's precedence selects: -E over -c over -shared, and an
310
+ # executable when no mode flag is present.
311
+ def mode
312
+ @mode_flag || :executable
313
+ end
314
+
315
+ # --- compile-only (-c) -------------------------------------------------
316
+
317
+ # Compiles each source to its own object. gcc rejects `-o` with `-c` when
318
+ # more than one source is compiled (one `-o` cannot name several outputs);
319
+ # with a single source `-o` names its object, otherwise the object is the
320
+ # source's basename with a `.o` suffix, dropped into the current directory.
321
+ def compile_only
322
+ sources = @inputs.select { |input| input[:kind] == :source }
323
+ warn_unused_link_inputs
324
+ if @output && sources.length > 1
325
+ raise UsageError, "cannot specify '-o' with '-c' and multiple compilations"
326
+ end
327
+
328
+ sources.each do |input|
329
+ output = @output || default_object_name(input[:path])
330
+ Compiler.compile_file(input[:path], output, include_paths: @include_paths,
331
+ pic: @pic, defines: @defines,
332
+ system_includes: @system_includes,
333
+ target: target, libc: libc,
334
+ default_visibility: @default_visibility)
335
+ end
336
+ end
337
+
338
+ # A `.o`/`.a`/`.so` passed alongside `-c` cannot contribute to a
339
+ # compile-only run; gcc keeps it a warning rather than an error.
340
+ def warn_unused_link_inputs
341
+ @inputs.each do |input|
342
+ next if input[:kind] == :source
343
+
344
+ warning("#{input[:path]}: linker input file unused because linking not done")
345
+ end
346
+ end
347
+
348
+ def default_object_name(path)
349
+ base = File.basename(path)
350
+ ext = File.extname(base)
351
+ "#{ext.empty? ? base : base[0...-ext.length]}.o"
352
+ end
353
+
354
+ # --- link (-shared / executable) ---------------------------------------
355
+
356
+ # Links every input into the requested output. Sources are compiled to object
357
+ # bytes in memory and passed straight to the linker; objects and archives are
358
+ # passed by path; a `.so` input becomes a dependency. The `-l`/`-L` requests
359
+ # are resolved into further archive inputs (appended after the objects so the
360
+ # lazy archive pull-in sees the objects' undefined symbols first) and the
361
+ # dependency shared objects imports bind against. rubycc's compiler-support
362
+ # runtime is appended last, after the user inputs and the resolved libraries,
363
+ # so its members are pulled in lazily only when something ahead still leaves
364
+ # one of their symbols undefined (the libgcc-style default link). The output
365
+ # is made executable so it can be run in place, as gcc leaves its own output.
366
+ def link
367
+ output = @output || "a.out"
368
+ link_inputs = []
369
+ needed = []
370
+ @inputs.each do |input|
371
+ case input[:kind]
372
+ when :source then link_inputs << compile_source(input)
373
+ when :object, :archive then link_inputs << input[:path]
374
+ when :shared then needed << input[:path]
375
+ end
376
+ end
377
+
378
+ resolution = Link::LibraryResolver.resolve(@libraries, search_dirs: @lib_dirs, target: target)
379
+ link_inputs.concat(resolution.inputs)
380
+ needed.concat(resolution.needed)
381
+ link_inputs << Link::CompatRuntime.archive_bytes(target: target) if @default_libs
382
+
383
+ if mode == :shared
384
+ Link::SharedLinker.link_to(link_inputs, output, needed: needed, soname: @soname)
385
+ else
386
+ Link::ExecutableLinker.link_to(link_inputs, output, needed: needed)
387
+ end
388
+ File.chmod(0o755, output)
389
+ end
390
+
391
+ def compile_source(input)
392
+ Compiler.new.compile(File.read(input[:path]), filename: input[:path],
393
+ include_paths: @include_paths, pic: @pic, defines: @defines,
394
+ system_includes: @system_includes, target: target, libc: libc,
395
+ default_visibility: @default_visibility)
396
+ end
397
+
398
+ # --- preprocess-only (-E) ----------------------------------------------
399
+
400
+ # Runs the preprocessor over each source and writes the token stream to `-o`
401
+ # (or standard output). The reconstruction is a best-effort re-spelling of the
402
+ # preprocessing tokens — token spellings joined with a space where whitespace
403
+ # separated them and a newline where the line advanced — rather than a
404
+ # byte-faithful copy of gcc's `-E` output; it is enough for a probe that only
405
+ # needs macros expanded and headers included. Non-source inputs are ignored.
406
+ def preprocess_only
407
+ text = +""
408
+ @inputs.each do |input|
409
+ next unless input[:kind] == :source
410
+
411
+ tokens = preprocessor_for_target.preprocess(
412
+ File.read(input[:path]), filename: input[:path],
413
+ include_paths: @include_paths, defines: @defines,
414
+ system_includes: @system_includes
415
+ )
416
+ text << render_preprocessed(tokens.reject(&:eof?))
417
+ end
418
+
419
+ if @output
420
+ File.write(@output, text)
421
+ else
422
+ @out.print(text)
423
+ end
424
+ end
425
+
426
+ # A preprocessor configured for the selected target and libc, so `-E`
427
+ # predefines the same macros a full compile of the same command line would.
428
+ # An unknown target falls back to the x86-64 entry: `-E` never reaches
429
+ # #ensure_supported_target, and reporting a bad -target as a preprocessing
430
+ # crash would be worse than preprocessing under the default identity.
431
+ def preprocessor_for_target
432
+ entry = Compiler::TARGETS.fetch(target) { Compiler::TARGETS.fetch("x86_64") }
433
+ Preprocess::Preprocessor.new(char_unsigned: !entry[:char_signed],
434
+ arch_macros: entry[:arch_macros],
435
+ libc_arch: entry[:libc_arch],
436
+ libc: libc)
437
+ end
438
+
439
+ def render_preprocessed(tokens)
440
+ out = +""
441
+ previous = nil
442
+ tokens.each do |token|
443
+ if previous.nil?
444
+ # first token: no leading separator
445
+ elsif token.filename != previous.filename || token.line != previous.line
446
+ out << "\n"
447
+ elsif token.space_before
448
+ out << " "
449
+ end
450
+ out << token.text
451
+ previous = token
452
+ end
453
+ out << "\n" unless out.empty?
454
+ out
455
+ end
456
+
457
+ # --- diagnostics -------------------------------------------------------
458
+
459
+ def warning(message)
460
+ @err.puts "#{PROG}: warning: #{message}"
461
+ end
462
+ end
463
+ end