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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +61 -0
- data/README.md +26 -14
- data/data/verified_gems.json +85 -63
- data/exe/rubycc-ar +11 -3
- data/include/libc/sys/cdefs.h +12 -0
- data/lib/rubycc/backend/aarch64.rb +705 -117
- data/lib/rubycc/backend/slot_residency.rb +169 -0
- data/lib/rubycc/backend/x86_64.rb +924 -137
- data/lib/rubycc/command_line.rb +339 -0
- data/lib/rubycc/compile_error.rb +6 -3
- data/lib/rubycc/compiler.rb +17 -2
- data/lib/rubycc/diagnostics.rb +105 -0
- data/lib/rubycc/doctor/gemfile.rb +12 -3
- data/lib/rubycc/doctor/verified_gems.rb +5 -1
- data/lib/rubycc/driver.rb +66 -10
- data/lib/rubycc/front/ast.rb +18 -7
- data/lib/rubycc/front/constant_evaluator.rb +12 -0
- data/lib/rubycc/front/lexeme_reader.rb +3 -1
- data/lib/rubycc/front/parser.rb +51 -18
- data/lib/rubycc/ir/analysis.rb +82 -0
- data/lib/rubycc/ir/call_convention.rb +74 -7
- data/lib/rubycc/ir/generator.rb +319 -9
- data/lib/rubycc/ir/ir.rb +39 -1
- data/lib/rubycc/ir/promotion.rb +255 -0
- data/lib/rubycc/ir/simplify.rb +570 -0
- data/lib/rubycc/link/library_resolver.rb +17 -5
- data/lib/rubycc/link/partial_linker.rb +8 -1
- data/lib/rubycc/link/shared_linker.rb +2 -2
- data/lib/rubycc/mkmf_shim.rb +178 -12
- data/lib/rubycc/objfile/ar_archive.rb +13 -2
- data/lib/rubycc/objfile/elf_reader.rb +13 -2
- data/lib/rubycc/pkgconf/parser.rb +4 -0
- data/lib/rubycc/pkgconf/resolver.rb +3 -1
- data/lib/rubycc/pkgconf/system_path_filter.rb +8 -2
- data/lib/rubycc/preprocess/preprocessor.rb +161 -37
- data/lib/rubycc/preprocess/scanner.rb +69 -14
- data/lib/rubycc/preprocess/token_converter.rb +11 -1
- data/lib/rubycc/rmake/cli.rb +32 -4
- data/lib/rubycc/rmake/executor.rb +157 -226
- data/lib/rubycc/rmake/makefile.rb +35 -13
- data/lib/rubycc/rmake/parser.rb +8 -2
- data/lib/rubycc/rmake/rmake.rb +1 -0
- data/lib/rubycc/rmake/tool_command.rb +69 -0
- data/lib/rubycc/shell.rb +510 -0
- data/lib/rubycc/type.rb +23 -7
- data/lib/rubycc/version.rb +1 -1
- data/lib/rubycc.rb +12 -0
- data/lib/rubygems_plugin.rb +31 -3
- metadata +14 -3
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubycc
|
|
4
|
+
# The base error is normally provided by lib/rubycc.rb. This file is loadable
|
|
5
|
+
# on its own (the mkmf shim pulls it in without the compiler behind it), so
|
|
6
|
+
# define a stand-in only when the full library has not been required yet.
|
|
7
|
+
Error = Class.new(StandardError) unless defined?(Error)
|
|
8
|
+
|
|
9
|
+
# Splitting a command line the way a POSIX shell would — and *only* the part a
|
|
10
|
+
# shell does before it execs: word splitting and quote removal. The point is to
|
|
11
|
+
# run a command that was written as one string without /bin/sh, which the
|
|
12
|
+
# minimal target environment does not have (DESIGN R5).
|
|
13
|
+
#
|
|
14
|
+
# Two callers share this: rmake, which interprets Makefile recipe lines
|
|
15
|
+
# (connectors, redirections, `VAR=value` prefixes and all), and the mkmf shim,
|
|
16
|
+
# which turns the single conftest command string mkmf builds into an argv array
|
|
17
|
+
# before mkmf spawns it. Both need exactly the same word-splitting rules, so
|
|
18
|
+
# the splitter lives here rather than in either of them.
|
|
19
|
+
#
|
|
20
|
+
# Nothing here expands anything: no variables, no globs, no command
|
|
21
|
+
# substitution, and no grammar above one command's words — `for` and `if`
|
|
22
|
+
# belong to the layer above (Rubycc::Shell), which calls this one on the
|
|
23
|
+
# simple commands it has already cut out of a line. A construct that would
|
|
24
|
+
# need a shell to interpret raises UnsupportedSyntaxError — the caller reports
|
|
25
|
+
# it. Falling back to a shell is not an option we keep in reserve: a fallback
|
|
26
|
+
# would make the result depend on whether a shell happens to exist, which is
|
|
27
|
+
# the very thing this code exists to remove.
|
|
28
|
+
module CommandLine
|
|
29
|
+
# A construct the splitter does not interpret (a pipe, background `&`,
|
|
30
|
+
# command substitution, an unterminated quote, ...). It names the construct
|
|
31
|
+
# and the line it was found in; callers wrap it in their own error type with
|
|
32
|
+
# whatever context they have (rmake adds the target, the mkmf shim writes the
|
|
33
|
+
# reason to mkmf.log).
|
|
34
|
+
class UnsupportedSyntaxError < Rubycc::Error
|
|
35
|
+
attr_reader :construct, :command
|
|
36
|
+
|
|
37
|
+
def initialize(construct, command)
|
|
38
|
+
@construct = construct
|
|
39
|
+
@command = command
|
|
40
|
+
super("unsupported shell construct (#{construct}): #{command.inspect}")
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# One redirection parsed off a command: which stream (:stdout/:stderr),
|
|
45
|
+
# whether it truncates or appends, and the target path (relative to the
|
|
46
|
+
# command's cwd).
|
|
47
|
+
Redirection = Struct.new(:stream, :mode, :path)
|
|
48
|
+
|
|
49
|
+
# One simple command: leading `VAR=value` assignments, the argv words and
|
|
50
|
+
# the redirections that apply to it.
|
|
51
|
+
SimpleCommand = Struct.new(:assignments, :argv, :redirections)
|
|
52
|
+
|
|
53
|
+
# Characters after which a backslash inside double quotes keeps its
|
|
54
|
+
# special meaning (POSIX quote removal, XCU 2.2 / 2.2.3): only these five
|
|
55
|
+
# make the backslash consume — and remove itself along with — the next
|
|
56
|
+
# character; before a newline both vanish (line continuation). Before any
|
|
57
|
+
# other character the backslash inside double quotes is left in the word
|
|
58
|
+
# verbatim: `"a\b"` stays `a\b`, while `"a\"b"` becomes `a"b`. Verified
|
|
59
|
+
# against /bin/sh (dash).
|
|
60
|
+
DQUOTE_BACKSLASH_SPECIAL = ["$", "`", '"', "\\", "\n"].freeze
|
|
61
|
+
|
|
62
|
+
# The word that names each connector in a diagnostic.
|
|
63
|
+
CONNECTOR_NAMES = { and: "&&", or: "||", semi: ";" }.freeze
|
|
64
|
+
|
|
65
|
+
# Shell reserved words that introduce a compound command Rubycc::Shell has a
|
|
66
|
+
# grammar for (the `for` loop, `if`, the brace group). They are still not
|
|
67
|
+
# something *this* module can make sense of — it splits one command's words,
|
|
68
|
+
# and these words are the seams between commands — so the single-command
|
|
69
|
+
# entry point (#argv) keeps refusing them. #parse does not: it is what Shell
|
|
70
|
+
# calls once it has cut a simple command out of the line, by which point the
|
|
71
|
+
# keywords are gone.
|
|
72
|
+
COMPOUND_WORDS = %w[for do done if then elif else fi { }].freeze
|
|
73
|
+
|
|
74
|
+
# Reserved words nothing here interprets: no layer of rubycc has a grammar
|
|
75
|
+
# for `case`, `while`, `until` or `!`, so they are refused wherever they
|
|
76
|
+
# appear in the command-name position. Only the unambiguous ones are listed:
|
|
77
|
+
# `in`, `time` and `select` are left out because they double as ordinary
|
|
78
|
+
# words often enough that flagging them would misfire. Checked only in the
|
|
79
|
+
# command-name position (see #parse) — `echo while` is a plain word.
|
|
80
|
+
RESERVED_WORDS = %w[case esac while until !].freeze
|
|
81
|
+
|
|
82
|
+
module_function
|
|
83
|
+
|
|
84
|
+
# Split +text+ into tokens: :word (quote-stripped), the connectors
|
|
85
|
+
# :and/:or/:semi and :redirect markers. Single quotes protect everything
|
|
86
|
+
# verbatim — backslash has no special meaning inside them. Double quotes
|
|
87
|
+
# strip a backslash only before `$`/`` ` ``/`"`/`\`/newline
|
|
88
|
+
# (DQUOTE_BACKSLASH_SPECIAL); elsewhere the backslash stays in the word.
|
|
89
|
+
# Outside any quote, a backslash preserves the literal value of the
|
|
90
|
+
# following character and disappears itself, except before a newline
|
|
91
|
+
# where both vanish (line continuation) — this is POSIX quote removal,
|
|
92
|
+
# verified against /bin/sh. mkmf relies on the outside-quotes rule: it
|
|
93
|
+
# writes `-DSYSCONFDIR=\"...\"` expecting the shell to unescape the
|
|
94
|
+
# backslash-quotes into a literal `"` in the word. Genuinely unhandled
|
|
95
|
+
# shell syntax (pipe, background, substitution, subshell) stops the split.
|
|
96
|
+
def tokenize(text)
|
|
97
|
+
tokenize_spans(text).map(&:first)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# #tokenize's split, with each token paired with the `start...stop` range it
|
|
101
|
+
# occupied in +text+. Rubycc::Shell needs to get the *source* of a word back
|
|
102
|
+
# after the split: a POSIX shell recognises words first and only then expands
|
|
103
|
+
# parameters in them (XCU 2.1), and whether `$x` is inside quotes decides
|
|
104
|
+
# whether its value is field-split. Quote removal has already thrown that
|
|
105
|
+
# away by the time #tokenize returns a word, so Shell parses the structure
|
|
106
|
+
# from these tokens, cuts each simple command out of +text+ by its span,
|
|
107
|
+
# expands it, and calls #parse on the result.
|
|
108
|
+
def tokenize_spans(text)
|
|
109
|
+
tokens = []
|
|
110
|
+
word = nil
|
|
111
|
+
word_start = nil
|
|
112
|
+
i = 0
|
|
113
|
+
n = text.length
|
|
114
|
+
# Close off the word being accumulated, if any, at +stop+.
|
|
115
|
+
flush = lambda do |stop|
|
|
116
|
+
next if word.nil?
|
|
117
|
+
|
|
118
|
+
tokens << [[:word, word], word_start...stop]
|
|
119
|
+
word = nil
|
|
120
|
+
word_start = nil
|
|
121
|
+
end
|
|
122
|
+
# Start (or continue) a word at the current position.
|
|
123
|
+
begin_word = lambda { word_start ||= i }
|
|
124
|
+
|
|
125
|
+
while i < n
|
|
126
|
+
c = text[i]
|
|
127
|
+
case c
|
|
128
|
+
when "'"
|
|
129
|
+
close = text.index(c, i + 1)
|
|
130
|
+
unsupported!("unterminated quote", text) if close.nil?
|
|
131
|
+
begin_word.call
|
|
132
|
+
word = (word || +"") + text[(i + 1)...close]
|
|
133
|
+
i = close + 1
|
|
134
|
+
when '"'
|
|
135
|
+
begin_word.call
|
|
136
|
+
segment, i = scan_double_quoted(text, i)
|
|
137
|
+
word = (word || +"") + segment
|
|
138
|
+
when "\\"
|
|
139
|
+
nxt = text[i + 1]
|
|
140
|
+
begin_word.call
|
|
141
|
+
if nxt.nil?
|
|
142
|
+
# A lone trailing backslash with nothing to escape is kept
|
|
143
|
+
# literally (verified against /bin/sh).
|
|
144
|
+
word = (word || +"") + c
|
|
145
|
+
i += 1
|
|
146
|
+
elsif nxt == "\n"
|
|
147
|
+
i += 2 # line continuation: backslash and newline both vanish
|
|
148
|
+
# Nothing was added to the word; if it is still empty the span must
|
|
149
|
+
# not claim to have started here.
|
|
150
|
+
word_start = nil if word.nil?
|
|
151
|
+
else
|
|
152
|
+
word = (word || +"") + nxt
|
|
153
|
+
i += 2
|
|
154
|
+
end
|
|
155
|
+
when " ", "\t"
|
|
156
|
+
flush.call(i)
|
|
157
|
+
i += 1
|
|
158
|
+
when "&"
|
|
159
|
+
flush.call(i)
|
|
160
|
+
unsupported!("background '&'", text) unless text[i + 1] == "&"
|
|
161
|
+
tokens << [[:and], i...(i + 2)]
|
|
162
|
+
i += 2
|
|
163
|
+
when "|"
|
|
164
|
+
flush.call(i)
|
|
165
|
+
unsupported!("pipe '|'", text) unless text[i + 1] == "|"
|
|
166
|
+
tokens << [[:or], i...(i + 2)]
|
|
167
|
+
i += 2
|
|
168
|
+
when ";"
|
|
169
|
+
flush.call(i)
|
|
170
|
+
tokens << [[:semi], i...(i + 1)]
|
|
171
|
+
i += 1
|
|
172
|
+
when ">"
|
|
173
|
+
flush.call(i)
|
|
174
|
+
if text[i + 1] == ">"
|
|
175
|
+
tokens << [[:redirect, :stdout, :append], i...(i + 2)]
|
|
176
|
+
i += 2
|
|
177
|
+
else
|
|
178
|
+
tokens << [[:redirect, :stdout, :truncate], i...(i + 1)]
|
|
179
|
+
i += 1
|
|
180
|
+
end
|
|
181
|
+
when "<", "`", "(", ")"
|
|
182
|
+
unsupported!("shell metacharacter '#{c}'", text)
|
|
183
|
+
else
|
|
184
|
+
if word.nil? && (c == "1" || c == "2") && text[i + 1] == ">"
|
|
185
|
+
stream = c == "2" ? :stderr : :stdout
|
|
186
|
+
if text[i + 2] == ">"
|
|
187
|
+
tokens << [[:redirect, stream, :append], i...(i + 3)]
|
|
188
|
+
i += 3
|
|
189
|
+
else
|
|
190
|
+
tokens << [[:redirect, stream, :truncate], i...(i + 2)]
|
|
191
|
+
i += 2
|
|
192
|
+
end
|
|
193
|
+
else
|
|
194
|
+
begin_word.call
|
|
195
|
+
word = (word || +"") + c
|
|
196
|
+
i += 1
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
flush.call(n)
|
|
201
|
+
tokens
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Scan a double-quoted segment starting at +i+ (text[i] == '"'). Applies
|
|
205
|
+
# the backslash-removal rule that is special to double quotes (see
|
|
206
|
+
# DQUOTE_BACKSLASH_SPECIAL) and returns [content, index_after_closing_quote].
|
|
207
|
+
def scan_double_quoted(text, i)
|
|
208
|
+
n = text.length
|
|
209
|
+
j = i + 1
|
|
210
|
+
buf = +""
|
|
211
|
+
loop do
|
|
212
|
+
unsupported!("unterminated quote", text) if j >= n
|
|
213
|
+
|
|
214
|
+
c = text[j]
|
|
215
|
+
if c == '"'
|
|
216
|
+
j += 1
|
|
217
|
+
break
|
|
218
|
+
elsif c == "\\" && j + 1 < n && DQUOTE_BACKSLASH_SPECIAL.include?(text[j + 1])
|
|
219
|
+
nxt = text[j + 1]
|
|
220
|
+
buf << nxt unless nxt == "\n" # backslash-newline vanishes entirely
|
|
221
|
+
j += 2
|
|
222
|
+
else
|
|
223
|
+
buf << c
|
|
224
|
+
j += 1
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
[buf, j]
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Turn +text+ into [[connector, SimpleCommand], ...]: the simple commands it
|
|
231
|
+
# holds, each tagged with the connector that precedes it (:first for the
|
|
232
|
+
# leading one, then :and/:or/:semi). A leading run of `VAR=value` words
|
|
233
|
+
# becomes that command's environment; a redirect marker consumes the
|
|
234
|
+
# following word as its target path.
|
|
235
|
+
def parse(text)
|
|
236
|
+
tokens = tokenize(text)
|
|
237
|
+
commands = []
|
|
238
|
+
connector = :first
|
|
239
|
+
assignments = []
|
|
240
|
+
argv = []
|
|
241
|
+
redirections = []
|
|
242
|
+
i = 0
|
|
243
|
+
|
|
244
|
+
flush = lambda do
|
|
245
|
+
unless assignments.empty? && argv.empty? && redirections.empty?
|
|
246
|
+
commands << [connector, SimpleCommand.new(assignments, argv, redirections)]
|
|
247
|
+
end
|
|
248
|
+
assignments = []
|
|
249
|
+
argv = []
|
|
250
|
+
redirections = []
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
while i < tokens.length
|
|
254
|
+
tok = tokens[i]
|
|
255
|
+
case tok[0]
|
|
256
|
+
when :word
|
|
257
|
+
w = tok[1]
|
|
258
|
+
if argv.empty? && w =~ /\A[A-Za-z_][A-Za-z0-9_]*=/
|
|
259
|
+
assignments << w
|
|
260
|
+
else
|
|
261
|
+
# A reserved word in the command-name position needs a shell
|
|
262
|
+
# grammar to make sense of. The ones a grammar now exists for
|
|
263
|
+
# (COMPOUND_WORDS) never reach here — Rubycc::Shell consumes them
|
|
264
|
+
# while cutting the line into simple commands, and calls this on
|
|
265
|
+
# what is left. The rest have no interpreter anywhere in rubycc, so
|
|
266
|
+
# they are refused: running one as a plain command would exec
|
|
267
|
+
# `while` itself and fail like a missing tool rather than like
|
|
268
|
+
# unsupported syntax, and approximating the construct is the
|
|
269
|
+
# shell-fallback risk this module exists to remove (see the file
|
|
270
|
+
# banner and mkmf-shell-free-conftest-1).
|
|
271
|
+
if argv.empty? && RESERVED_WORDS.include?(w)
|
|
272
|
+
unsupported!("shell reserved word '#{w}'", text)
|
|
273
|
+
end
|
|
274
|
+
argv << w
|
|
275
|
+
end
|
|
276
|
+
when :and, :or, :semi
|
|
277
|
+
flush.call
|
|
278
|
+
connector = tok[0]
|
|
279
|
+
when :redirect
|
|
280
|
+
nxt = tokens[i + 1]
|
|
281
|
+
unsupported!("redirection without a target", text) if nxt.nil? || nxt[0] != :word
|
|
282
|
+
redirections << Redirection.new(tok[1], tok[2], nxt[1])
|
|
283
|
+
i += 1
|
|
284
|
+
end
|
|
285
|
+
i += 1
|
|
286
|
+
end
|
|
287
|
+
flush.call
|
|
288
|
+
commands
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
# The argv array of +text+ read as one plain command — a program word and
|
|
292
|
+
# its arguments, nothing else. Anything a shell would have to interpret
|
|
293
|
+
# beyond word splitting and quote removal (a connector, a redirection, an
|
|
294
|
+
# environment assignment, an empty command) raises UnsupportedSyntaxError
|
|
295
|
+
# rather than being approximated. This is the mkmf shim's entry point: the
|
|
296
|
+
# conftest commands mkmf builds are plain commands, and one that is not has
|
|
297
|
+
# to be reported, not guessed at.
|
|
298
|
+
def argv(text)
|
|
299
|
+
commands = parse(text)
|
|
300
|
+
unsupported!("empty command", text) if commands.empty?
|
|
301
|
+
if commands.length > 1
|
|
302
|
+
unsupported!("command connector '#{CONNECTOR_NAMES[commands[1][0]]}'", text)
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
command = commands.first[1]
|
|
306
|
+
# A compound keyword is refused here even though #parse now lets it
|
|
307
|
+
# through: this entry point promises *one plain command*, and a caller
|
|
308
|
+
# that has no shell grammar (the mkmf shim) must not exec a `for`.
|
|
309
|
+
unsupported!("shell reserved word '#{command.argv.first}'", text) \
|
|
310
|
+
if COMPOUND_WORDS.include?(command.argv.first)
|
|
311
|
+
unsupported!("redirection", text) unless command.redirections.empty?
|
|
312
|
+
unsupported!("environment assignment '#{command.assignments.first}'", text) \
|
|
313
|
+
unless command.assignments.empty?
|
|
314
|
+
|
|
315
|
+
command.argv
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# The inverse of the splitter for one word: +word+ spelled so that #argv
|
|
319
|
+
# (and a POSIX shell, and Shellwords, which is what RubyGems splits
|
|
320
|
+
# `ENV["MAKE"]` with) reads it back as this exact single word. Needed because
|
|
321
|
+
# a tool command has to be carried as *one string* — `CC = <ruby> <exe>` in a
|
|
322
|
+
# Makefile, `ENV["MAKE"]` for RubyGems — and an installation path is allowed
|
|
323
|
+
# to contain a space.
|
|
324
|
+
#
|
|
325
|
+
# A word of ordinary path characters is left alone, so the common case stays
|
|
326
|
+
# readable; anything else is single-quoted (which protects every character
|
|
327
|
+
# but a single quote), and a word containing a single quote is spelled with
|
|
328
|
+
# the `'\''` idiom every one of those splitters understands.
|
|
329
|
+
def quote(word)
|
|
330
|
+
return word if word.match?(/\A[A-Za-z0-9_@%+=:,.\/-]+\z/)
|
|
331
|
+
|
|
332
|
+
"'" + word.gsub("'", %q('"'"')) + "'"
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def unsupported!(construct, text)
|
|
336
|
+
raise UnsupportedSyntaxError.new(construct, text)
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
end
|
data/lib/rubycc/compile_error.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "diagnostics"
|
|
4
|
+
|
|
3
5
|
module Rubycc
|
|
4
6
|
# Base error class (also defined in rubycc.rb; reopened here so this file can
|
|
5
7
|
# be required standalone).
|
|
@@ -27,10 +29,11 @@ module Rubycc
|
|
|
27
29
|
|
|
28
30
|
private
|
|
29
31
|
|
|
32
|
+
# Rendered by the shared diagnostic formatter, so an error and a warning
|
|
33
|
+
# (Diagnostics.warn) differ only in the severity word.
|
|
30
34
|
def build_message
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
"#{header}\n#{source_line}\n#{caret}"
|
|
35
|
+
Diagnostics.render("error", description, filename: filename, line: line,
|
|
36
|
+
column: column, source_line: source_line)
|
|
34
37
|
end
|
|
35
38
|
end
|
|
36
39
|
end
|
data/lib/rubycc/compiler.rb
CHANGED
|
@@ -5,6 +5,8 @@ require_relative "compile_error"
|
|
|
5
5
|
require_relative "preprocess/preprocessor"
|
|
6
6
|
require_relative "front/parser"
|
|
7
7
|
require_relative "ir/generator"
|
|
8
|
+
require_relative "ir/analysis"
|
|
9
|
+
require_relative "ir/simplify"
|
|
8
10
|
require_relative "backend/x86_64"
|
|
9
11
|
require_relative "backend/aarch64"
|
|
10
12
|
require_relative "objfile/elf_writer"
|
|
@@ -148,7 +150,19 @@ module Rubycc
|
|
|
148
150
|
defined_names = Set.new
|
|
149
151
|
relocations = []
|
|
150
152
|
ir_program.functions.each do |ir_func|
|
|
151
|
-
|
|
153
|
+
# The local rewrites (IR::Simplify) sit here, between the generator and
|
|
154
|
+
# the backend, rather than inside either: the generator's output stays
|
|
155
|
+
# the plain lowering of the source, which is what makes it readable and
|
|
156
|
+
# testable, and a backend keeps receiving one well-defined IR rather
|
|
157
|
+
# than having to run the pass itself. Both backends lower everything the
|
|
158
|
+
# pass can produce, so the seam needs no per-target condition.
|
|
159
|
+
#
|
|
160
|
+
# The census the pass took on its way through travels with the function
|
|
161
|
+
# (IR::Analysis), because the backend needs the same counts — the
|
|
162
|
+
# transient set, and through IR::Promotion the occurrence weights — and
|
|
163
|
+
# counting the list again is the one thing this seam can save it.
|
|
164
|
+
analysis = IR::Analysis.simplified(ir_func)
|
|
165
|
+
result = backend.compile(analysis.function, analysis)
|
|
152
166
|
# Align each function to 16 bytes with the target's NOP filler, keeping
|
|
153
167
|
# the output deterministic and every entry point aligned.
|
|
154
168
|
pad_to_alignment(text, 16, entry[:machine].text_padding)
|
|
@@ -293,7 +307,8 @@ module Rubycc
|
|
|
293
307
|
system_includes: true, target: "x86_64",
|
|
294
308
|
libc: Preprocess::Preprocessor.host_libc,
|
|
295
309
|
default_visibility: :default)
|
|
296
|
-
|
|
310
|
+
# Bytes, not locale-encoded text: see Scanner's class comment.
|
|
311
|
+
source = File.binread(input_path)
|
|
297
312
|
binary = new.compile(source, filename: input_path, include_paths: include_paths,
|
|
298
313
|
pic: pic, defines: defines, system_includes: system_includes,
|
|
299
314
|
target: target, libc: libc,
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubycc
|
|
4
|
+
# The compiler's diagnostic channel: one renderer for every severity, and the
|
|
5
|
+
# stream the non-fatal ones are written to.
|
|
6
|
+
#
|
|
7
|
+
# Until `#warning` there was only one severity here. A diagnostic was a raised
|
|
8
|
+
# CompileError and nothing else, so "report it" and "give up on this
|
|
9
|
+
# translation unit" were the same act, and several places in the front end say
|
|
10
|
+
# so ("a warning is not a channel this compiler has"). `#warning` needs the
|
|
11
|
+
# other half: a located message that is reported and then *returned from*, so
|
|
12
|
+
# the compile continues and exits 0 (C23 6.10.2p2, and gcc's long-standing
|
|
13
|
+
# extension of the same shape).
|
|
14
|
+
#
|
|
15
|
+
# The two halves share #render, so a warning is spelled exactly like an error
|
|
16
|
+
# apart from the severity word — one place to change the format, not two.
|
|
17
|
+
# CompileError renders itself through it (see compile_error.rb); a warning is
|
|
18
|
+
# written straight to the stream, because there is nothing to raise.
|
|
19
|
+
#
|
|
20
|
+
# The stream is process-wide state rather than a parameter threaded through
|
|
21
|
+
# every component, for the reason the first caller shows: the warning is
|
|
22
|
+
# raised deep inside the preprocessor's directive walk, whose callers
|
|
23
|
+
# (Compiler, and the parser and IR generator that will want this channel next)
|
|
24
|
+
# take no stream and have no business growing one. Concurrency does not argue
|
|
25
|
+
# against it here — rmake's `-j` isolates each step in a forked child (see
|
|
26
|
+
# Rmake::Executor), so no two translation units share this state.
|
|
27
|
+
module Diagnostics
|
|
28
|
+
# The stream state when nobody has chosen one: warnings go to the process's
|
|
29
|
+
# $stderr, looked up at emit time so a test (or a caller) that swaps $stderr
|
|
30
|
+
# is honored. It is a distinct object from nil, which means "discard".
|
|
31
|
+
INHERIT = Object.new
|
|
32
|
+
private_constant :INHERIT
|
|
33
|
+
|
|
34
|
+
# The stream lives in thread-local storage rather than in a class variable.
|
|
35
|
+
# Nothing in this compiler runs two Drivers in one process today (rmake's
|
|
36
|
+
# `-j` forks each step, and a forked worker runs its Driver in-process but
|
|
37
|
+
# alone), so a shared slot would be correct as things stand. Thread-local
|
|
38
|
+
# costs one lookup and removes the whole class of bug the moment somebody
|
|
39
|
+
# does thread a second compile through here.
|
|
40
|
+
KEY = :rubycc_diagnostics_stream
|
|
41
|
+
private_constant :KEY
|
|
42
|
+
|
|
43
|
+
# Thread-local storage cannot tell "set to nil" from "never set", and the
|
|
44
|
+
# difference matters here: nil is a caller asking to discard, an absent slot
|
|
45
|
+
# means inherit $stderr. DISCARD carries the request instead.
|
|
46
|
+
DISCARD = Object.new
|
|
47
|
+
private_constant :DISCARD
|
|
48
|
+
|
|
49
|
+
class << self
|
|
50
|
+
# A gcc-style diagnostic: the "file:line:column: severity: text" header,
|
|
51
|
+
# the offending source line, and a caret under the offending column.
|
|
52
|
+
# `severity` is the bare word ("error", "warning").
|
|
53
|
+
#
|
|
54
|
+
# The message is assembled as *bytes*. Two of its pieces come from outside
|
|
55
|
+
# and carry whatever encoding their origin gave them: the source line is
|
|
56
|
+
# ASCII-8BIT (the scanner reads bytes, see Preprocess::Scanner), while the
|
|
57
|
+
# file name — and, through a header name or a macro spelling, sometimes the
|
|
58
|
+
# description — comes from the command line and is tagged with the locale's
|
|
59
|
+
# encoding. Splicing two such strings with plain interpolation raises
|
|
60
|
+
# Encoding::CompatibilityError as soon as both hold a byte past 0x7F
|
|
61
|
+
# (compiling "日本.c" under a UTF-8 locale, with a comment in Japanese on
|
|
62
|
+
# the offending line), which would replace the diagnostic with a Ruby
|
|
63
|
+
# backtrace — the very failure this renderer exists to avoid. Re-tagging
|
|
64
|
+
# each piece as bytes removes the question: nothing is transcoded, the
|
|
65
|
+
# bytes reach the terminal as they were spelled in the source, and the
|
|
66
|
+
# caret still counts in characters (see Scanner#column_at).
|
|
67
|
+
def render(severity, description, filename:, line:, column:, source_line:)
|
|
68
|
+
message = +"".b
|
|
69
|
+
message << filename.to_s.b << ":#{line}:#{column}: #{severity}: " << description.to_s.b
|
|
70
|
+
message << "\n" << source_line.to_s.b << "\n" << (" " * (column - 1)) << "^"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Reports a warning at a source position and returns; the compile goes on.
|
|
74
|
+
# Nothing is written when warnings are being discarded (see .to).
|
|
75
|
+
def warn(description, filename:, line:, column:, source_line:)
|
|
76
|
+
target = stream
|
|
77
|
+
return if target.nil?
|
|
78
|
+
|
|
79
|
+
target.puts render("warning", description, filename: filename, line: line,
|
|
80
|
+
column: column, source_line: source_line)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Runs the block with warnings written to `stream` — an IO, or nil to
|
|
84
|
+
# discard them (what the driver's `-w` selects). The previous target is
|
|
85
|
+
# restored afterwards, so a nested run (rmake invoking the Driver
|
|
86
|
+
# in-process) leaves nothing behind.
|
|
87
|
+
def to(stream)
|
|
88
|
+
previous = Thread.current.thread_variable_get(KEY) || INHERIT
|
|
89
|
+
Thread.current.thread_variable_set(KEY, stream.nil? ? DISCARD : stream)
|
|
90
|
+
yield
|
|
91
|
+
ensure
|
|
92
|
+
Thread.current.thread_variable_set(KEY, previous)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
private
|
|
96
|
+
|
|
97
|
+
def stream
|
|
98
|
+
current = Thread.current.thread_variable_get(KEY) || INHERIT
|
|
99
|
+
return $stderr if current.equal?(INHERIT)
|
|
100
|
+
|
|
101
|
+
current.equal?(DISCARD) ? nil : current
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -32,12 +32,15 @@ module Rubycc
|
|
|
32
32
|
# Load gem information from +dir+ (or an explicit --gemfile path). A
|
|
33
33
|
# Gemfile.lock next to the Gemfile is preferred; otherwise the Gemfile
|
|
34
34
|
# itself is parsed. Returns a Result, or nil when neither file exists.
|
|
35
|
+
#
|
|
36
|
+
# Read as bytes (lib/rubycc.rb): a Gemfile that comments in its author's
|
|
37
|
+
# own language is ordinary, and the doctor is the first command anyone runs.
|
|
35
38
|
def load(gemfile_path)
|
|
36
39
|
lock = "#{gemfile_path}.lock"
|
|
37
40
|
if File.file?(lock)
|
|
38
|
-
parse_lock(File.
|
|
41
|
+
parse_lock(File.binread(lock), path: lock)
|
|
39
42
|
elsif File.file?(gemfile_path)
|
|
40
|
-
parse_gemfile(File.
|
|
43
|
+
parse_gemfile(File.binread(gemfile_path), path: gemfile_path)
|
|
41
44
|
end
|
|
42
45
|
end
|
|
43
46
|
|
|
@@ -47,7 +50,11 @@ module Rubycc
|
|
|
47
50
|
# 4-space indent (`name (version)`), each optionally trailed by its own
|
|
48
51
|
# dependencies at 6-space indent (`dep (constraint)`). DEPENDENCIES lists
|
|
49
52
|
# the directly-declared gems at 2-space indent.
|
|
53
|
+
#
|
|
54
|
+
# Bytes in (lib/rubycc.rb): every construct named above is spelled in
|
|
55
|
+
# ASCII, and the rest is carried through.
|
|
50
56
|
def parse_lock(text, path: nil)
|
|
57
|
+
text = text.b unless text.encoding == Encoding::BINARY
|
|
51
58
|
entries = []
|
|
52
59
|
direct = []
|
|
53
60
|
section = nil # current top-level section header
|
|
@@ -96,8 +103,10 @@ module Rubycc
|
|
|
96
103
|
# declaration and an optional literal version argument. This never evaluates
|
|
97
104
|
# the file, so conditionals, variables and computed names are invisible —
|
|
98
105
|
# the caller reports the reduced fidelity. Group/platform blocks are not
|
|
99
|
-
# tracked; every declared gem is returned.
|
|
106
|
+
# tracked; every declared gem is returned. Bytes in, like #parse_lock: the
|
|
107
|
+
# comment this strips is exactly where a Gemfile's non-ASCII lives.
|
|
100
108
|
def parse_gemfile(text, path: nil)
|
|
109
|
+
text = text.b unless text.encoding == Encoding::BINARY
|
|
101
110
|
entries = []
|
|
102
111
|
text.each_line do |raw|
|
|
103
112
|
line = raw.sub(/#.*/, "")
|
|
@@ -46,8 +46,12 @@ module Rubycc
|
|
|
46
46
|
end
|
|
47
47
|
|
|
48
48
|
# Load the database from +path+ (defaults to the shipped file).
|
|
49
|
+
#
|
|
50
|
+
# The encoding is named rather than inherited from the locale: JSON *is*
|
|
51
|
+
# UTF-8 (RFC 8259 §8.1), so there is nothing to guess. The shipped file's
|
|
52
|
+
# notes carry non-ASCII punctuation, which a locale-tagged read rejects.
|
|
49
53
|
def self.load(path = DEFAULT_PATH)
|
|
50
|
-
new(JSON.parse(File.read(path)))
|
|
54
|
+
new(JSON.parse(File.read(path, encoding: Encoding::UTF_8)))
|
|
51
55
|
end
|
|
52
56
|
|
|
53
57
|
def initialize(raw)
|