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,570 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "set"
|
|
4
|
+
require_relative "ir"
|
|
5
|
+
|
|
6
|
+
module Rubycc
|
|
7
|
+
module IR
|
|
8
|
+
# Local rewrites applied to a function's instruction list on its way from
|
|
9
|
+
# the generator to a backend. Everything here is decided from the flat list
|
|
10
|
+
# alone — a census of how often each virtual register is written and read,
|
|
11
|
+
# plus a look at the neighbouring instruction — so no control-flow graph,
|
|
12
|
+
# live range or interference graph is built. That boundary is deliberate:
|
|
13
|
+
# register allocation is a separate piece of work, and the transformations
|
|
14
|
+
# below are the ones that pay off *before* it, because they delete
|
|
15
|
+
# instructions an allocator would otherwise faithfully allocate registers
|
|
16
|
+
# for.
|
|
17
|
+
#
|
|
18
|
+
# Three rewrites run, in this order:
|
|
19
|
+
#
|
|
20
|
+
# 1. Single-use copy forwarding. Assigning to a variable lands the
|
|
21
|
+
# expression in a temporary and copies the temporary into the
|
|
22
|
+
# variable's slot; when that copy is the temporary's only reader, the
|
|
23
|
+
# producer can write the variable's slot itself and the copy goes.
|
|
24
|
+
# 2. Subscript fusion. The generator lowers "p[i]" as a multiply of the
|
|
25
|
+
# index by the element size followed by an add to the base, and the
|
|
26
|
+
# element size arrives as a :const of its own — three instructions,
|
|
27
|
+
# each costing a slot round trip. Both targets have a single
|
|
28
|
+
# instruction for exactly this shape (x86-64's `lea` with a SIB scale,
|
|
29
|
+
# AArch64's add with a shifted register operand), so the pair collapses
|
|
30
|
+
# into one :scaled_add and the constant is left with no readers.
|
|
31
|
+
# 3. Dead result elimination, which is what then removes that constant —
|
|
32
|
+
# and the value of a discarded "i++", and anything else the generator
|
|
33
|
+
# materialized into a slot nobody reads. Only side-effect-free
|
|
34
|
+
# instructions are candidates (see PURE_OPS); a division that may trap,
|
|
35
|
+
# a load that may fault and anything that writes memory or calls stay
|
|
36
|
+
# put whether or not their result is read.
|
|
37
|
+
#
|
|
38
|
+
# The pass is fail-safe by construction: it needs to know every place a
|
|
39
|
+
# virtual register can be *read*, and an op it does not recognize means it
|
|
40
|
+
# cannot know that, so #run hands the function back untouched rather than
|
|
41
|
+
# guessing (see #each_operand_vreg).
|
|
42
|
+
#
|
|
43
|
+
# The census the three rewrites decide from — how often each virtual
|
|
44
|
+
# register is read and written — is taken **once**, by #census, and then
|
|
45
|
+
# kept true by each rewrite as it goes: forwarding a copy removes exactly
|
|
46
|
+
# one read and one write of the temporary, fusing a subscript removes the
|
|
47
|
+
# reads of the pair it replaces, and dropping a dead instruction removes
|
|
48
|
+
# its own. What comes out is therefore the census of the list that comes
|
|
49
|
+
# out, which is what IR::Analysis hands on to IR::Promotion and to the
|
|
50
|
+
# backend so neither has to count the list again.
|
|
51
|
+
module Simplify
|
|
52
|
+
module_function
|
|
53
|
+
|
|
54
|
+
# Rewrites `function`, returning it unchanged when nothing applies.
|
|
55
|
+
# `vreg_count` is deliberately left alone: dropping an instruction must
|
|
56
|
+
# not renumber slots, both because the backends address slots by number
|
|
57
|
+
# and because a stable frame layout is what keeps the same source
|
|
58
|
+
# producing the same bytes (N4).
|
|
59
|
+
def run(function)
|
|
60
|
+
run_counted(function).first
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# #run plus the census of the list it decided on: [function, reads,
|
|
64
|
+
# writes]. `reads` and `writes` are arrays indexed by virtual register
|
|
65
|
+
# number — the numbers are dense and small, so an array is what a hash
|
|
66
|
+
# was standing in for — and both are nil when an unrecognized op made the
|
|
67
|
+
# pass refuse the function, which is the same answer as "nothing here may
|
|
68
|
+
# be counted on" for every later consumer (IR::Analysis).
|
|
69
|
+
def run_counted(function)
|
|
70
|
+
insts = function.insts
|
|
71
|
+
reads, writes = census(insts, function.vreg_count)
|
|
72
|
+
return [function, nil, nil] if reads.nil?
|
|
73
|
+
|
|
74
|
+
rewritten = drop_dead_results(
|
|
75
|
+
fuse_subscripts(forward_single_use_copies(insts, reads, writes), reads, writes),
|
|
76
|
+
reads, writes
|
|
77
|
+
)
|
|
78
|
+
# Each rewrite hands its own array back untouched when it changed
|
|
79
|
+
# nothing, so an untouched function really does come back as the very
|
|
80
|
+
# objects the generator produced, and is handed on unwrapped.
|
|
81
|
+
return [function, reads, writes] if rewritten.equal?(insts)
|
|
82
|
+
|
|
83
|
+
[Function.new(function.name, rewritten, function.vreg_count, function.param_count,
|
|
84
|
+
function.stack_objects, function.linkage, function.variadic,
|
|
85
|
+
function.param_kinds),
|
|
86
|
+
reads, writes]
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# The element sizes a subscript's scale factor may have. Both targets
|
|
90
|
+
# encode the scale as a shift of the index register, so these four powers
|
|
91
|
+
# of two are exactly what fits in one instruction; any other stride keeps
|
|
92
|
+
# its multiply.
|
|
93
|
+
SCALES = [1, 2, 4, 8].freeze
|
|
94
|
+
|
|
95
|
+
# Ops whose `a` and `b` fields are both plain virtual registers.
|
|
96
|
+
TWO_OPERAND_OPS = %i[
|
|
97
|
+
add sub mul mulhi div mod udiv umod and or xor shl sar shr
|
|
98
|
+
eq ne lt le gt ge ult ule ugt uge
|
|
99
|
+
fadd fsub fmul fdiv feq fne flt fle fgt fge
|
|
100
|
+
scaled_add store memcpy atomic_store
|
|
101
|
+
].freeze
|
|
102
|
+
|
|
103
|
+
# Ops whose `a` is a virtual register and whose `b`, if used at all, is
|
|
104
|
+
# something else (a label id, a width descriptor, a scan direction, a
|
|
105
|
+
# parameter count).
|
|
106
|
+
ONE_OPERAND_OPS = %i[
|
|
107
|
+
copy neg sext zext ftof itof ftoi load uload addr_of alloca
|
|
108
|
+
bit_scan popcount va_start atomic_load jump_if_zero
|
|
109
|
+
].freeze
|
|
110
|
+
|
|
111
|
+
# Ops that read no virtual register at all: their `a` is an immediate, a
|
|
112
|
+
# label id, an object id, a string-pool id or a symbol name.
|
|
113
|
+
NO_OPERAND_OPS = %i[
|
|
114
|
+
const label jump func_addr object_addr string_addr global_addr got_addr
|
|
115
|
+
atomic_fence
|
|
116
|
+
].freeze
|
|
117
|
+
|
|
118
|
+
# Ops whose operands need a shape of their own (a call's argument pairs, a
|
|
119
|
+
# return's struct buffer, an atomic's packed second operand).
|
|
120
|
+
IRREGULAR_OPS = %i[call call_indirect ret atomic_rmw atomic_cas].freeze
|
|
121
|
+
|
|
122
|
+
# The ops whose only effect is to put a value in `dst`: dropping one when
|
|
123
|
+
# nothing reads that value cannot change what the program does. The list
|
|
124
|
+
# is a whitelist on purpose. :div/:mod and their unsigned forms are absent
|
|
125
|
+
# because a zero divisor traps; :load/:uload because a wild pointer
|
|
126
|
+
# faults; every memory write, call, atomic and :alloca because the effect
|
|
127
|
+
# *is* the point.
|
|
128
|
+
PURE_OPS = %i[
|
|
129
|
+
const copy add sub mul mulhi and or xor shl sar shr neg
|
|
130
|
+
eq ne lt le gt ge ult ule ugt uge
|
|
131
|
+
fadd fsub fmul fdiv feq fne flt fle fgt fge itof ftoi ftof
|
|
132
|
+
sext zext bit_scan popcount scaled_add
|
|
133
|
+
addr_of object_addr string_addr global_addr func_addr got_addr
|
|
134
|
+
].freeze
|
|
135
|
+
|
|
136
|
+
# The four groups above as one op -> shape table. The arrays are the
|
|
137
|
+
# documentation, one line per op in reading order; this is what the scans
|
|
138
|
+
# ask, a single hash probe per instruction rather than up to four linear
|
|
139
|
+
# searches through thirty symbols. The irregular ops each get a shape of
|
|
140
|
+
# their own, their operands living in fields no other op uses.
|
|
141
|
+
OPERAND_SHAPES = {}.tap do |shapes|
|
|
142
|
+
TWO_OPERAND_OPS.each { |op| shapes[op] = :two }
|
|
143
|
+
ONE_OPERAND_OPS.each { |op| shapes[op] = :one }
|
|
144
|
+
NO_OPERAND_OPS.each { |op| shapes[op] = :none }
|
|
145
|
+
IRREGULAR_OPS.each { |op| shapes[op] = op }
|
|
146
|
+
end.freeze
|
|
147
|
+
|
|
148
|
+
def known_op?(op)
|
|
149
|
+
OPERAND_SHAPES.key?(op)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Yields every virtual register `inst` reads, nils skipped and repeats
|
|
153
|
+
# kept ("t + t" reads t twice). Getting this exhaustive is what the whole
|
|
154
|
+
# pass rests on — a missed read would let a live value be deleted — so the
|
|
155
|
+
# shapes are enumerated rather than inferred, and #run refuses to touch a
|
|
156
|
+
# function containing an op not listed above (which yields nothing here).
|
|
157
|
+
#
|
|
158
|
+
# It yields rather than returning the list it used to return because it
|
|
159
|
+
# runs once per instruction per scan, and that list was garbage every
|
|
160
|
+
# time — one array per instruction per pass, on a path the GC already
|
|
161
|
+
# dominates. #reads_vreg? is the one question a caller asked the list
|
|
162
|
+
# rather than the elements.
|
|
163
|
+
def each_operand_vreg(inst)
|
|
164
|
+
case OPERAND_SHAPES[inst.op]
|
|
165
|
+
when :two
|
|
166
|
+
a = inst.a
|
|
167
|
+
b = inst.b
|
|
168
|
+
yield a unless a.nil?
|
|
169
|
+
yield b unless b.nil?
|
|
170
|
+
when :one
|
|
171
|
+
a = inst.a
|
|
172
|
+
yield a unless a.nil?
|
|
173
|
+
when :none
|
|
174
|
+
nil
|
|
175
|
+
when :call, :call_indirect
|
|
176
|
+
# Each argument is a [vreg, kind] pair, whose vreg is nil for an
|
|
177
|
+
# alignment pad; an indirect call also reads its target, and a struct
|
|
178
|
+
# result read back in registers is scattered into a buffer whose
|
|
179
|
+
# address is a further read (the second half of the size pair).
|
|
180
|
+
inst.b.each { |vreg, _kind| yield vreg unless vreg.nil? }
|
|
181
|
+
yield inst.a if inst.op == :call_indirect
|
|
182
|
+
ret = inst.size&.last
|
|
183
|
+
yield ret.first if ret.is_a?(Array)
|
|
184
|
+
when :ret
|
|
185
|
+
a = inst.a
|
|
186
|
+
yield a unless a.nil?
|
|
187
|
+
when :atomic_rmw
|
|
188
|
+
yield inst.a
|
|
189
|
+
yield inst.b[0]
|
|
190
|
+
when :atomic_cas
|
|
191
|
+
yield inst.a
|
|
192
|
+
yield inst.b[0]
|
|
193
|
+
yield inst.b[1]
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Whether `inst` reads `vreg` — #each_operand_vreg's membership test,
|
|
198
|
+
# without the list it would have had to build to answer.
|
|
199
|
+
def reads_vreg?(inst, vreg)
|
|
200
|
+
each_operand_vreg(inst) { |operand| return true if operand == vreg }
|
|
201
|
+
false
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# How often each virtual register is read and written, as the pair of
|
|
205
|
+
# arrays [reads, writes] indexed by register number, or nil when some op
|
|
206
|
+
# is not one this file knows how to read (the fail-safe #run rests on,
|
|
207
|
+
# tested here because this is the one scan every caller starts from).
|
|
208
|
+
#
|
|
209
|
+
# `vreg_count` only sizes the arrays: a register past it still counts,
|
|
210
|
+
# growing them, so a hand-built function with a loose count is answered
|
|
211
|
+
# the same as a generated one.
|
|
212
|
+
#
|
|
213
|
+
# This scan and the ones below walk the list by index rather than with
|
|
214
|
+
# each_with_index, which is the one place in this file where speed decided
|
|
215
|
+
# the shape: the block call per instruction was measured at a sixth of the
|
|
216
|
+
# whole back half of the compiler (stackprof, bigdecimal.c, 2026-08-15).
|
|
217
|
+
def census(insts, vreg_count = 0)
|
|
218
|
+
reads = Array.new(vreg_count, 0)
|
|
219
|
+
writes = Array.new(vreg_count, 0)
|
|
220
|
+
index = 0
|
|
221
|
+
size = insts.size
|
|
222
|
+
while index < size
|
|
223
|
+
inst = insts[index]
|
|
224
|
+
index += 1
|
|
225
|
+
return nil unless OPERAND_SHAPES.key?(inst.op)
|
|
226
|
+
|
|
227
|
+
dst = inst.dst
|
|
228
|
+
unless dst.nil?
|
|
229
|
+
count = writes[dst]
|
|
230
|
+
writes[dst] = count ? count + 1 : 1
|
|
231
|
+
end
|
|
232
|
+
each_operand_vreg(inst) do |vreg|
|
|
233
|
+
count = reads[vreg]
|
|
234
|
+
reads[vreg] = count ? count + 1 : 1
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
[reads, writes]
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Maps each virtual register written by exactly one :const to
|
|
241
|
+
# [that constant's value, the index of the instruction that sets it].
|
|
242
|
+
# A register written more than once, or written by anything else, is
|
|
243
|
+
# absent — as is one whose slot address is taken, since a store through
|
|
244
|
+
# that address can replace the value the :const put there. That last
|
|
245
|
+
# exclusion is the same aliasing rule the backends' residency tracking
|
|
246
|
+
# obeys, applied at instruction granularity because here there is no
|
|
247
|
+
# emitted-nothing test to lean on.
|
|
248
|
+
def constant_definitions(insts)
|
|
249
|
+
constants = {}
|
|
250
|
+
rejected = {}
|
|
251
|
+
index = 0
|
|
252
|
+
size = insts.size
|
|
253
|
+
while index < size
|
|
254
|
+
inst = insts[index]
|
|
255
|
+
op = inst.op
|
|
256
|
+
rejected[inst.a] = true if op == :addr_of
|
|
257
|
+
dst = inst.dst
|
|
258
|
+
unless dst.nil?
|
|
259
|
+
if op == :const && !constants.key?(dst)
|
|
260
|
+
constants[dst] = [inst.a, index]
|
|
261
|
+
else
|
|
262
|
+
rejected[dst] = true
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
index += 1
|
|
266
|
+
end
|
|
267
|
+
return constants if rejected.empty?
|
|
268
|
+
|
|
269
|
+
constants.reject { |vreg, _| rejected[vreg] }
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# Rewrites "T = <anything>; V = T" as "V = <anything>" whenever T is
|
|
273
|
+
# written once, read once, and that one read is the copy immediately
|
|
274
|
+
# behind it. Assigning to a variable goes through such a pair — the
|
|
275
|
+
# expression lands in a temporary and the temporary is copied into the
|
|
276
|
+
# variable's slot — so this removes a whole slot round trip per
|
|
277
|
+
# assignment, which in a floating-point loop is three instructions each
|
|
278
|
+
# time (the value is stored from a vector register and copied through a
|
|
279
|
+
# general-purpose one).
|
|
280
|
+
#
|
|
281
|
+
# Writing V one instruction earlier changes nothing, the two being
|
|
282
|
+
# adjacent, and it stays correct when the producer *reads* V as well ("i =
|
|
283
|
+
# i + 1" becomes an add whose destination is its own operand): every
|
|
284
|
+
# backend loads an instruction's operands into registers before it stores
|
|
285
|
+
# the result.
|
|
286
|
+
#
|
|
287
|
+
# `reads` and `writes` come from #census and are brought up to date as the
|
|
288
|
+
# rewrite goes: the pair "T = ...; V = T" becomes one instruction writing
|
|
289
|
+
# V, so T loses its single read and its single write and nothing else
|
|
290
|
+
# moves. Updating them in flight cannot change a later decision, which is
|
|
291
|
+
# what makes keeping the census incrementally the same pass as counting it
|
|
292
|
+
# afresh would have been: the guard has just established that this pair is
|
|
293
|
+
# T's only reader and only writer, so no instruction the loop has yet to
|
|
294
|
+
# reach mentions T at all.
|
|
295
|
+
def forward_single_use_copies(insts, reads, writes)
|
|
296
|
+
forwarded = nil
|
|
297
|
+
index = 0
|
|
298
|
+
size = insts.size
|
|
299
|
+
while index < size
|
|
300
|
+
inst = insts[index]
|
|
301
|
+
dst = inst.dst
|
|
302
|
+
copy = insts[index + 1]
|
|
303
|
+
if dst && copy && copy.op == :copy && copy.a == dst && copy.dst != dst &&
|
|
304
|
+
reads[dst] == 1 && writes[dst] == 1
|
|
305
|
+
forwarded ||= insts[0, index]
|
|
306
|
+
forwarded << Instruction.new(inst.op, dst: copy.dst, a: inst.a, b: inst.b, size: inst.size)
|
|
307
|
+
reads[dst] -= 1
|
|
308
|
+
writes[dst] -= 1
|
|
309
|
+
index += 2 # the copy is the second half of what was just written
|
|
310
|
+
else
|
|
311
|
+
forwarded << inst if forwarded
|
|
312
|
+
index += 1
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
forwarded || insts
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# Collapses "index * element_size" followed by "base + that" into one
|
|
319
|
+
# :scaled_add. The two must be adjacent, which is how the generator
|
|
320
|
+
# actually emits a subscript and is also what makes the rewrite obviously
|
|
321
|
+
# safe: with nothing in between, neither operand of the add can have been
|
|
322
|
+
# rewritten since the multiply read it.
|
|
323
|
+
#
|
|
324
|
+
# The census is kept true the same way #forward_single_use_copies keeps
|
|
325
|
+
# it: the two instructions the :scaled_add replaces stop reading what they
|
|
326
|
+
# read and it starts reading what it reads. Here, though, the decisions
|
|
327
|
+
# are deliberately taken from `census`, which stops following `reads` the
|
|
328
|
+
# moment the first fusion would disturb it — a loop may read a value the
|
|
329
|
+
# list defines further down, so a fusion *can* lower the read count a
|
|
330
|
+
# later fusion's guard consults, and the answer has to stay the one the
|
|
331
|
+
# pre-pass count gave.
|
|
332
|
+
def fuse_subscripts(insts, reads, writes)
|
|
333
|
+
constants = constant_definitions(insts)
|
|
334
|
+
return insts if constants.empty?
|
|
335
|
+
|
|
336
|
+
census = reads
|
|
337
|
+
fused = nil
|
|
338
|
+
index = 0
|
|
339
|
+
size = insts.size
|
|
340
|
+
while index < size
|
|
341
|
+
inst = insts[index]
|
|
342
|
+
add = insts[index + 1]
|
|
343
|
+
replacement = scaled_add_for(inst, add, index, constants, census)
|
|
344
|
+
if replacement
|
|
345
|
+
census = reads.dup if census.equal?(reads)
|
|
346
|
+
fused ||= insts[0, index]
|
|
347
|
+
fused << replacement
|
|
348
|
+
each_operand_vreg(inst) { |vreg| reads[vreg] -= 1 }
|
|
349
|
+
each_operand_vreg(add) { |vreg| reads[vreg] -= 1 }
|
|
350
|
+
each_operand_vreg(replacement) { |vreg| reads[vreg] += 1 }
|
|
351
|
+
writes[inst.dst] -= 1 # the add's destination is the replacement's
|
|
352
|
+
index += 2 # the add is the second half of the :scaled_add
|
|
353
|
+
else
|
|
354
|
+
fused << inst if fused
|
|
355
|
+
index += 1
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
fused || insts
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
# The :scaled_add that replaces `mul` and the `add` right behind it, or
|
|
362
|
+
# nil when the pair is not a subscript. The conditions are:
|
|
363
|
+
#
|
|
364
|
+
# * both are 8-byte (pointer-width) operations — a narrower multiply is
|
|
365
|
+
# ordinary arithmetic, not address forming;
|
|
366
|
+
# * one multiply operand is a constant 1/2/4/8 defined earlier;
|
|
367
|
+
# * the add reads the multiply's result, and is its *only* reader, so
|
|
368
|
+
# removing the multiply leaves the product unwanted;
|
|
369
|
+
# * the add's other operand is a different register (a "t + t" would
|
|
370
|
+
# have read the product twice and is excluded by the count anyway).
|
|
371
|
+
def scaled_add_for(mul, add, index, constants, reads)
|
|
372
|
+
return nil unless mul.op == :mul && mul.size == 8
|
|
373
|
+
return nil unless add && add.op == :add && add.size == 8
|
|
374
|
+
return nil unless reads[mul.dst] == 1
|
|
375
|
+
return nil unless add.a == mul.dst || add.b == mul.dst
|
|
376
|
+
|
|
377
|
+
scale, index_vreg = scale_operand(mul, index, constants)
|
|
378
|
+
return nil unless SCALES.include?(scale)
|
|
379
|
+
|
|
380
|
+
base = add.a == mul.dst ? add.b : add.a
|
|
381
|
+
Instruction.new(:scaled_add, dst: add.dst, a: base, b: index_vreg, size: scale)
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
# Splits a multiply into [constant factor, the other operand] when one
|
|
385
|
+
# side is a constant set before instruction `index`, else [nil, nil]. The
|
|
386
|
+
# position test is what makes reading the constant's value here sound: the
|
|
387
|
+
# :const has already run wherever this multiply is reached from.
|
|
388
|
+
def scale_operand(mul, index, constants)
|
|
389
|
+
b_const = constants[mul.b]
|
|
390
|
+
return [b_const[0], mul.a] if b_const && b_const[1] < index
|
|
391
|
+
|
|
392
|
+
a_const = constants[mul.a]
|
|
393
|
+
return [a_const[0], mul.b] if a_const && a_const[1] < index
|
|
394
|
+
|
|
395
|
+
[nil, nil]
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
# --- transient values --------------------------------------------------
|
|
399
|
+
#
|
|
400
|
+
# A virtual register whose slot never has to be written at all: the value
|
|
401
|
+
# is produced by one instruction, read by the very next one, and read
|
|
402
|
+
# nowhere else, so it can simply stay in the register its producer left it
|
|
403
|
+
# in. Every intermediate result of an expression is of this shape, which is
|
|
404
|
+
# why a spill-everything backend writes so many slots nobody ever reads —
|
|
405
|
+
# in "b[i] += scale * a[i]" nine of the twelve stores in the loop are these.
|
|
406
|
+
#
|
|
407
|
+
# This is deliberately *not* a liveness computation. Four conditions, all
|
|
408
|
+
# decided from the flat list, make the answer sound without one:
|
|
409
|
+
#
|
|
410
|
+
# * exactly one instruction writes the register and exactly one reads it,
|
|
411
|
+
# so there is no other definition to reach a reader and no other reader
|
|
412
|
+
# to reach;
|
|
413
|
+
# * the reader is the instruction immediately after the writer. Nothing
|
|
414
|
+
# can be interposed, and because every branch target in this IR is a
|
|
415
|
+
# :label — which is never a reader — control cannot enter between them
|
|
416
|
+
# or reach the reader without having run the writer;
|
|
417
|
+
# * the register is not one of the parameter slots, which the prologue
|
|
418
|
+
# writes before any instruction runs;
|
|
419
|
+
# * producer and consumer agree on which register file the value lives
|
|
420
|
+
# in, and at which width (see #vector_result_width). A double computed
|
|
421
|
+
# into a vector register is no use to a reader that expects it in a
|
|
422
|
+
# general-purpose one.
|
|
423
|
+
#
|
|
424
|
+
# The last gate is the whitelist below. An op only qualifies as a producer
|
|
425
|
+
# if its final act is to store its result, and only as a consumer if it
|
|
426
|
+
# reads its operands before disturbing anything — which is why a call is
|
|
427
|
+
# neither: staging its arguments overwrites the very register the value
|
|
428
|
+
# would be waiting in.
|
|
429
|
+
PRODUCER_OPS = %i[
|
|
430
|
+
const copy add sub mul mulhi and or xor shl sar shr neg
|
|
431
|
+
eq ne lt le gt ge ult ule ugt uge
|
|
432
|
+
fadd fsub fmul fdiv feq fne flt fle fgt fge itof ftoi ftof
|
|
433
|
+
sext zext bit_scan popcount scaled_add load uload
|
|
434
|
+
div mod udiv umod alloca
|
|
435
|
+
addr_of object_addr string_addr global_addr func_addr got_addr
|
|
436
|
+
].freeze
|
|
437
|
+
|
|
438
|
+
CONSUMER_OPS = %i[
|
|
439
|
+
copy add sub mul mulhi and or xor shl sar shr neg
|
|
440
|
+
eq ne lt le gt ge ult ule ugt uge
|
|
441
|
+
fadd fsub fmul fdiv feq fne flt fle fgt fge itof ftoi ftof
|
|
442
|
+
sext zext bit_scan popcount scaled_add load uload store memcpy
|
|
443
|
+
div mod udiv umod alloca jump_if_zero ret
|
|
444
|
+
].freeze
|
|
445
|
+
|
|
446
|
+
# The three whitelists above, and PURE_OPS, as lookup tables. The arrays
|
|
447
|
+
# are the documentation — one line per op, in reading order, which is how
|
|
448
|
+
# each of them is read and argued about — and these are what the scans ask
|
|
449
|
+
# once per instruction.
|
|
450
|
+
PURE = PURE_OPS.to_h { |op| [op, true] }.freeze
|
|
451
|
+
PRODUCERS = PRODUCER_OPS.to_h { |op| [op, true] }.freeze
|
|
452
|
+
CONSUMERS = CONSUMER_OPS.to_h { |op| [op, true] }.freeze
|
|
453
|
+
|
|
454
|
+
# Which virtual registers a backend may leave in a register instead of
|
|
455
|
+
# writing out, as an array indexed by register number holding true for a
|
|
456
|
+
# transient and nil for everything else — the form the backends ask on
|
|
457
|
+
# every store they emit. `param_count` names the leading slots the
|
|
458
|
+
# prologue fills; `reads` and `writes` are #census of this very list.
|
|
459
|
+
def transient_flags(insts, param_count, reads, writes, vreg_count = 0)
|
|
460
|
+
flags = Array.new(vreg_count)
|
|
461
|
+
index = 0
|
|
462
|
+
size = insts.size
|
|
463
|
+
while index < size
|
|
464
|
+
inst = insts[index]
|
|
465
|
+
index += 1
|
|
466
|
+
vreg = inst.dst
|
|
467
|
+
next if vreg.nil? || vreg < param_count
|
|
468
|
+
next unless reads[vreg] == 1 && writes[vreg] == 1
|
|
469
|
+
next unless PRODUCERS.key?(inst.op)
|
|
470
|
+
|
|
471
|
+
reader = insts[index]
|
|
472
|
+
next unless reader && CONSUMERS.key?(reader.op)
|
|
473
|
+
next unless reads_vreg?(reader, vreg)
|
|
474
|
+
next unless vector_result_width(inst) == vector_operand_width(reader, vreg)
|
|
475
|
+
|
|
476
|
+
flags[vreg] = true
|
|
477
|
+
end
|
|
478
|
+
flags
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
# The same answer as a set of register numbers, for a caller that has no
|
|
482
|
+
# census in hand and wants to read the result rather than index it.
|
|
483
|
+
def transient_vregs(insts, param_count)
|
|
484
|
+
reads, writes = census(insts)
|
|
485
|
+
return Set.new if reads.nil?
|
|
486
|
+
|
|
487
|
+
transient = Set.new
|
|
488
|
+
transient_flags(insts, param_count, reads, writes).each_with_index do |flag, vreg|
|
|
489
|
+
transient << vreg if flag
|
|
490
|
+
end
|
|
491
|
+
transient
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
# The width at which `inst` leaves its result in a vector register, or nil
|
|
495
|
+
# when the result goes to a general-purpose one. :ftof is the one op whose
|
|
496
|
+
# `size` names its *source*, so its destination is the other width.
|
|
497
|
+
def vector_result_width(inst)
|
|
498
|
+
case inst.op
|
|
499
|
+
when :fadd, :fsub, :fmul, :fdiv, :itof then inst.size
|
|
500
|
+
when :ftof then inst.size == 8 ? 4 : 8
|
|
501
|
+
end
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
# The width at which `inst` reads `vreg` out of a vector register, or nil
|
|
505
|
+
# when it reads it into a general-purpose one. A float comparison's result
|
|
506
|
+
# is an int but its operands are floats; :itof is the mirror image, and
|
|
507
|
+
# :ret carries the width in `size` only when returning a float or double
|
|
508
|
+
# (an integer return leaves it nil, a struct return an array of pieces).
|
|
509
|
+
def vector_operand_width(inst, _vreg)
|
|
510
|
+
case inst.op
|
|
511
|
+
when :fadd, :fsub, :fmul, :fdiv, :feq, :fne, :flt, :fle, :fgt, :fge, :ftoi, :ftof
|
|
512
|
+
inst.size
|
|
513
|
+
when :ret
|
|
514
|
+
inst.size if inst.size.is_a?(Integer)
|
|
515
|
+
end
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
# Drops every pure instruction whose result nothing reads, repeating until
|
|
519
|
+
# nothing more falls out: removing one instruction removes its own reads,
|
|
520
|
+
# which can make the instruction that produced *those* values dead in
|
|
521
|
+
# turn (a fused subscript's element-size constant is reached in one round,
|
|
522
|
+
# the sign extension feeding a discarded "i++" in two).
|
|
523
|
+
#
|
|
524
|
+
# Each round subtracts the reads it drops as it drops them rather than
|
|
525
|
+
# counting the whole list again, which makes the passes over the list a
|
|
526
|
+
# function of what is actually dead instead of of the depth of the chain.
|
|
527
|
+
# Applying a subtraction inside the round it was made in can only bring a
|
|
528
|
+
# removal forward from the next round to this one, never add or lose one:
|
|
529
|
+
# an instruction with no readers keeps having none as more instructions
|
|
530
|
+
# go, so the process removes exactly the instructions no chain of readers
|
|
531
|
+
# reaches, whatever order it visits them in.
|
|
532
|
+
#
|
|
533
|
+
# `orphaned` is what ends the loop one round earlier than "a round that
|
|
534
|
+
# dropped nothing" would: an instruction can only have *become* dead
|
|
535
|
+
# through a subtraction that took one of its readers' counts to zero, so a
|
|
536
|
+
# round in which no count reached zero has left nothing behind for the
|
|
537
|
+
# next one to find. The commonest round of all is exactly that shape — the
|
|
538
|
+
# element-size :const a fused subscript orphans reads nothing itself — so
|
|
539
|
+
# the confirming pass over the whole list usually goes.
|
|
540
|
+
def drop_dead_results(insts, reads, writes)
|
|
541
|
+
loop do
|
|
542
|
+
kept = nil
|
|
543
|
+
orphaned = false
|
|
544
|
+
index = 0
|
|
545
|
+
size = insts.size
|
|
546
|
+
while index < size
|
|
547
|
+
inst = insts[index]
|
|
548
|
+
dst = inst.dst
|
|
549
|
+
if !dst.nil? && (reads[dst] || 0).zero? && PURE.key?(inst.op)
|
|
550
|
+
kept ||= insts[0, index]
|
|
551
|
+
each_operand_vreg(inst) do |vreg|
|
|
552
|
+
count = reads[vreg] - 1
|
|
553
|
+
reads[vreg] = count
|
|
554
|
+
orphaned = true if count.zero?
|
|
555
|
+
end
|
|
556
|
+
writes[dst] -= 1
|
|
557
|
+
elsif kept
|
|
558
|
+
kept << inst
|
|
559
|
+
end
|
|
560
|
+
index += 1
|
|
561
|
+
end
|
|
562
|
+
return insts if kept.nil?
|
|
563
|
+
|
|
564
|
+
insts = kept
|
|
565
|
+
return insts unless orphaned
|
|
566
|
+
end
|
|
567
|
+
end
|
|
568
|
+
end
|
|
569
|
+
end
|
|
570
|
+
end
|
|
@@ -138,9 +138,12 @@ module Rubycc
|
|
|
138
138
|
# (archives and objects) to feed the merge, both in first-seen order.
|
|
139
139
|
Resolution = Struct.new(:needed, :inputs, keyword_init: true)
|
|
140
140
|
|
|
141
|
+
# The search path is held as bytes (lib/rubycc.rb): `-L` operands carry the
|
|
142
|
+
# locale's encoding, while the names joined onto them come from linker
|
|
143
|
+
# scripts and directory listings.
|
|
141
144
|
def initialize(search_dirs: [], target: nil)
|
|
142
|
-
@dirs = (search_dirs + self.class.default_system_dirs(target: target)).
|
|
143
|
-
File.directory?(d)
|
|
145
|
+
@dirs = (search_dirs + self.class.default_system_dirs(target: target)).filter_map do |d|
|
|
146
|
+
d.b if File.directory?(d)
|
|
144
147
|
end
|
|
145
148
|
end
|
|
146
149
|
|
|
@@ -148,11 +151,13 @@ module Rubycc
|
|
|
148
151
|
# the existing default directories); exposed so a driver can report it.
|
|
149
152
|
attr_reader :dirs
|
|
150
153
|
|
|
154
|
+
# +libraries+ are the texts after `-l`, split off the command line; each is
|
|
155
|
+
# composed into a file name (`lib<name>.so`), so it is taken as bytes.
|
|
151
156
|
def resolve(libraries)
|
|
152
157
|
@needed = []
|
|
153
158
|
@inputs = []
|
|
154
159
|
@seen = Set.new
|
|
155
|
-
libraries.each { |spec| resolve_spec(spec) }
|
|
160
|
+
libraries.each { |spec| resolve_spec(spec.b) }
|
|
156
161
|
Resolution.new(needed: @needed, inputs: @inputs)
|
|
157
162
|
end
|
|
158
163
|
|
|
@@ -259,8 +264,10 @@ module Rubycc
|
|
|
259
264
|
end
|
|
260
265
|
end
|
|
261
266
|
|
|
267
|
+
# Entries come back tagged with the filesystem encoding; as bytes they can
|
|
268
|
+
# be joined onto #dirs whatever either of them holds.
|
|
262
269
|
def directory_entries(dir)
|
|
263
|
-
Dir.children(dir)
|
|
270
|
+
Dir.children(dir).map(&:b)
|
|
264
271
|
rescue SystemCallError
|
|
265
272
|
[]
|
|
266
273
|
end
|
|
@@ -306,7 +313,9 @@ module Rubycc
|
|
|
306
313
|
# `-l` token inside the script searches the path recursively and an absolute
|
|
307
314
|
# path is ingested directly.
|
|
308
315
|
def expand_script(path)
|
|
309
|
-
|
|
316
|
+
# Bytes: a script's comment names a distributor and its file list names
|
|
317
|
+
# paths, either of which can sit outside ASCII.
|
|
318
|
+
LinkerScript.parse(File.binread(path)).each { |token| resolve_token(token) }
|
|
310
319
|
end
|
|
311
320
|
|
|
312
321
|
# Resolves one token from a linker script's file list. A `-l` token is a
|
|
@@ -353,7 +362,10 @@ module Rubycc
|
|
|
353
362
|
end
|
|
354
363
|
end
|
|
355
364
|
|
|
365
|
+
# Bytes in (lib/rubycc.rb): every command this reader recognizes is spelled
|
|
366
|
+
# in ASCII, and the names it collects are paths.
|
|
356
367
|
def initialize(text)
|
|
368
|
+
text = text.b unless text.encoding == Encoding::BINARY
|
|
357
369
|
@tokens = tokenize(strip_comments(text))
|
|
358
370
|
end
|
|
359
371
|
|
|
@@ -89,12 +89,19 @@ module Rubycc
|
|
|
89
89
|
# Classifies one raw input. A String that already opens with an ELF or ar
|
|
90
90
|
# magic is taken as in-memory bytes; anything else is a filesystem path to
|
|
91
91
|
# read. The resulting bytes are then dispatched to the matching reader.
|
|
92
|
+
#
|
|
93
|
+
# A path is re-tagged as bytes on the way in, the boundary rule of
|
|
94
|
+
# lib/rubycc.rb: it is joined with an archive member's name to label a
|
|
95
|
+
# pulled-in member, and ArReader returns those names as bytes, which Ruby
|
|
96
|
+
# will not interpolate together with a path tagged otherwise once either
|
|
97
|
+
# side holds non-ASCII bytes. A label is diagnostic text and must never be
|
|
98
|
+
# the thing that raises.
|
|
92
99
|
def load_input(raw, index)
|
|
93
100
|
if raw.is_a?(String) && (raw.b.start_with?(ELFMAG) || raw.b.start_with?(AR_MAGIC))
|
|
94
101
|
bytes = raw.b
|
|
95
102
|
label = "input##{index}"
|
|
96
103
|
else
|
|
97
|
-
label = raw.to_s
|
|
104
|
+
label = raw.to_s.b
|
|
98
105
|
bytes = File.binread(label)
|
|
99
106
|
end
|
|
100
107
|
|
|
@@ -216,8 +216,8 @@ module Rubycc
|
|
|
216
216
|
# `__attribute__((destructor(0)))`, lands in the same section and, being
|
|
217
217
|
# ahead of the supplier in link order, ends up running *after* this slot
|
|
218
218
|
# rather than before it. gcc warns about that priority range for exactly
|
|
219
|
-
# this kind of reason; rubycc
|
|
220
|
-
# is simply implementation-defined.)
|
|
219
|
+
# this kind of reason; rubycc's linker warns about nothing, so the ordering
|
|
220
|
+
# there is simply implementation-defined.)
|
|
221
221
|
CXA_FINALIZE_SYMBOL = "__cxa_finalize"
|
|
222
222
|
DSO_FINALIZER_SYMBOL = "__rubycc_dso_finalize"
|
|
223
223
|
DSO_FINI_ARRAY_SECTION = ".fini_array.00000"
|