rubycc 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +61 -0
  3. data/README.md +26 -14
  4. data/data/verified_gems.json +85 -63
  5. data/exe/rubycc-ar +11 -3
  6. data/include/libc/sys/cdefs.h +12 -0
  7. data/lib/rubycc/backend/aarch64.rb +705 -117
  8. data/lib/rubycc/backend/slot_residency.rb +169 -0
  9. data/lib/rubycc/backend/x86_64.rb +924 -137
  10. data/lib/rubycc/command_line.rb +339 -0
  11. data/lib/rubycc/compile_error.rb +6 -3
  12. data/lib/rubycc/compiler.rb +17 -2
  13. data/lib/rubycc/diagnostics.rb +105 -0
  14. data/lib/rubycc/doctor/gemfile.rb +12 -3
  15. data/lib/rubycc/doctor/verified_gems.rb +5 -1
  16. data/lib/rubycc/driver.rb +66 -10
  17. data/lib/rubycc/front/ast.rb +18 -7
  18. data/lib/rubycc/front/constant_evaluator.rb +12 -0
  19. data/lib/rubycc/front/lexeme_reader.rb +3 -1
  20. data/lib/rubycc/front/parser.rb +51 -18
  21. data/lib/rubycc/ir/analysis.rb +82 -0
  22. data/lib/rubycc/ir/call_convention.rb +74 -7
  23. data/lib/rubycc/ir/generator.rb +319 -9
  24. data/lib/rubycc/ir/ir.rb +39 -1
  25. data/lib/rubycc/ir/promotion.rb +255 -0
  26. data/lib/rubycc/ir/simplify.rb +570 -0
  27. data/lib/rubycc/link/library_resolver.rb +17 -5
  28. data/lib/rubycc/link/partial_linker.rb +8 -1
  29. data/lib/rubycc/link/shared_linker.rb +2 -2
  30. data/lib/rubycc/mkmf_shim.rb +178 -12
  31. data/lib/rubycc/objfile/ar_archive.rb +13 -2
  32. data/lib/rubycc/objfile/elf_reader.rb +13 -2
  33. data/lib/rubycc/pkgconf/parser.rb +4 -0
  34. data/lib/rubycc/pkgconf/resolver.rb +3 -1
  35. data/lib/rubycc/pkgconf/system_path_filter.rb +8 -2
  36. data/lib/rubycc/preprocess/preprocessor.rb +161 -37
  37. data/lib/rubycc/preprocess/scanner.rb +69 -14
  38. data/lib/rubycc/preprocess/token_converter.rb +11 -1
  39. data/lib/rubycc/rmake/cli.rb +32 -4
  40. data/lib/rubycc/rmake/executor.rb +157 -226
  41. data/lib/rubycc/rmake/makefile.rb +35 -13
  42. data/lib/rubycc/rmake/parser.rb +8 -2
  43. data/lib/rubycc/rmake/rmake.rb +1 -0
  44. data/lib/rubycc/rmake/tool_command.rb +69 -0
  45. data/lib/rubycc/shell.rb +510 -0
  46. data/lib/rubycc/type.rb +23 -7
  47. data/lib/rubycc/version.rb +1 -1
  48. data/lib/rubycc.rb +12 -0
  49. data/lib/rubygems_plugin.rb +31 -3
  50. metadata +14 -3
@@ -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)).select do |d|
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
- LinkerScript.parse(File.read(path)).each { |token| resolve_token(token) }
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 has no warning channel, so the ordering there
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"