rpiet 0.1 → 0.2

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 (57) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +1 -0
  3. data/Gemfile +12 -0
  4. data/bin/color_wheel +84 -0
  5. data/bin/image_gen +39 -0
  6. data/bin/rpiet +68 -11
  7. data/images/counter.txt +7 -0
  8. data/lib/rpiet/asg/graph_interpreter.rb +39 -0
  9. data/lib/rpiet/asg/parser.rb +156 -0
  10. data/lib/rpiet/asg/visitor.rb +66 -0
  11. data/lib/rpiet/asg.rb +336 -0
  12. data/lib/rpiet/codel_chooser.rb +32 -4
  13. data/lib/rpiet/color.rb +70 -25
  14. data/lib/rpiet/cycle.rb +18 -7
  15. data/lib/rpiet/debugger/debugger.rb +298 -0
  16. data/lib/rpiet/debugger/stylesheet.css +88 -0
  17. data/lib/rpiet/direction_pointer.rb +49 -18
  18. data/lib/rpiet/event_handler.rb +62 -7
  19. data/lib/rpiet/group.rb +25 -8
  20. data/lib/rpiet/image/ascii_image.rb +8 -20
  21. data/lib/rpiet/image/image.rb +8 -3
  22. data/lib/rpiet/image/url_image.rb +28 -14
  23. data/lib/rpiet/interpreter.rb +72 -72
  24. data/lib/rpiet/ir/assembler.rb +87 -0
  25. data/lib/rpiet/ir/builder.rb +255 -0
  26. data/lib/rpiet/ir/cfg.rb +494 -0
  27. data/lib/rpiet/ir/instructions.rb +536 -0
  28. data/lib/rpiet/ir/ir_cfg_interpreter.rb +23 -0
  29. data/lib/rpiet/ir/ir_interpreter.rb +101 -0
  30. data/lib/rpiet/ir/ir_native_interpreter.rb +77 -0
  31. data/lib/rpiet/ir/jruby_backend.rb +279 -0
  32. data/lib/rpiet/ir/operands.rb +28 -0
  33. data/lib/rpiet/ir/passes/data_flow_problem.rb +32 -0
  34. data/lib/rpiet/ir/passes/flow_graph_node.rb +76 -0
  35. data/lib/rpiet/ir/passes/peephole.rb +214 -0
  36. data/lib/rpiet/ir/passes/push_pop_elimination_pass.rb +112 -0
  37. data/lib/rpiet/live_machine_state.rb +15 -0
  38. data/lib/rpiet/machine.rb +62 -32
  39. data/lib/rpiet/source.rb +83 -0
  40. data/lib/rpiet/version.rb +1 -1
  41. data/lib/rpiet.rb +2 -2
  42. data/rpiet.gemspec +19 -0
  43. data/spec/asg/visitor_spec.rb +41 -0
  44. data/spec/cycle_spec.rb +34 -34
  45. data/spec/direction_pointer_spec.rb +33 -6
  46. data/spec/group_spec.rb +73 -48
  47. data/spec/interpreter_spec.rb +161 -12
  48. data/spec/ir/assembler_spec.rb +122 -0
  49. data/spec/ir/builder_spec.rb +20 -0
  50. data/spec/ir/cfg_spec.rb +151 -0
  51. data/spec/ir/ir_interpreter_spec.rb +102 -0
  52. data/spec/ir/passes/push_pop_elimination_pass_spec.rb +34 -0
  53. data/spec/machine_spec.rb +5 -3
  54. data/spec/source_spec.rb +69 -0
  55. data/spec/spec_helper.rb +78 -0
  56. metadata +54 -16
  57. data/images/nfib.png +0 -0
@@ -0,0 +1,494 @@
1
+ require_relative 'instructions'
2
+ require 'rgl/adjacency'
3
+ require 'rgl/dot'
4
+ require 'rgl/edge_properties_map'
5
+ require 'rgl/traversal'
6
+
7
+ module RPiet
8
+ module IR
9
+ # Basic Block is a collection of instructions. It will commonly be referred to as bb
10
+ # in code and in comments. bbs is multiple basic blacks.
11
+ class BasicBlock
12
+ attr_reader :label, :instrs
13
+ attr_accessor :debug
14
+
15
+ def initialize(label)
16
+ @label, @instrs = label, []
17
+ end
18
+
19
+ def add_instr(instr) = @instrs << instr
20
+
21
+ def last_instr
22
+ instrs&.last
23
+ end
24
+
25
+ # If this bb only meaningfully contains a jump (e.g. jump + noops/label)
26
+ def only_contains_jump?
27
+ instrs&.last.class == Instructions::JumpInstr && !instrs[0...-2].find { |instr| !instr.noop? }
28
+ end
29
+
30
+ # Maybe a bit hacky but inspect is for dot output. consider changing this.
31
+ def inspect
32
+ str = "name: #{@label}\n\n"
33
+ str << "#{@instrs.map {|i| i}.join('\l')}\\l" if !@instrs.empty?
34
+ str
35
+ end
36
+ alias :to_s :inspect
37
+ end
38
+
39
+ # CFG - Control Flow Graph
40
+ #
41
+ # A control flow graph is a directed graph which show the all paths in which a
42
+ # program can flow/traverse. Having this form for your program opens up the
43
+ # ability to perform various compiler optimizations.
44
+ #
45
+ # An if statement, for example, shows a possible flow if the test stmt of the
46
+ # if succeeds. If it fails then the the flow moves past the if body. The if
47
+ # body itself flows to that same point:
48
+ #
49
+ # if test
50
+ # foo
51
+ # end
52
+ # bar
53
+ #
54
+ # +--------------------+ +----------------+
55
+ # | bb: start | true | bb: if_body |
56
+ # +--------------------+------------>>>+----------------+
57
+ # | branch_true test | | call foo |
58
+ # +--------------------+ +----------------+
59
+ # | |
60
+ # | false |
61
+ # | |
62
+ # v |
63
+ # +--------------------+ |
64
+ # | bb after_if | |
65
+ # +--------------------+<<<--------------------+
66
+ # | call bar |
67
+ # +--------------------+
68
+ #
69
+ # This CFG also generates an additional exit BB. This is the last flow node
70
+ # before execution terminates. Entry BB is the first node you encounter
71
+ # during execution and where you typically start on any analysis.
72
+ class CFG
73
+ attr_reader :entry_bb, :exit_bb
74
+
75
+ def initialize(instrs)
76
+ @graph, @bb_map, @edge_labels = RGL::DirectedAdjacencyGraph.new, {}, {}
77
+ build(instrs)
78
+ end
79
+
80
+ def basic_blocks
81
+ @bb_map.values
82
+ end
83
+
84
+ # Get a sequential list of instructions from this CFG that can be executed.
85
+ # This will linearize and optimize the CFG before making the instruction
86
+ # list.
87
+ def instructions(*passes)
88
+ passes.each do |pass|
89
+ pass.new(self).run
90
+ end
91
+
92
+ bbs = linearize
93
+
94
+ #write_to_dot_file
95
+ bbs.each_with_object([]) { |bb, arr| arr.concat bb.instrs }
96
+ end
97
+
98
+ def write_to_dot_file
99
+ @graph.write_to_graphic_file
100
+ end
101
+
102
+ def outgoing_edges(bb, edge_type=nil)
103
+ if block_given?
104
+ @graph.edges.each do |edge|
105
+ yield edge if outgoing_edges_match?(bb, edge, edge_type)
106
+ end
107
+ else
108
+ @graph.edges.select { |edge| outgoing_edges_match?(bb, edge, edge_type) }
109
+ end
110
+ end
111
+
112
+ def outgoing_target(bb, edge_type)
113
+ outgoing_edges(bb, edge_type).map { |edge| edge.target }&.first
114
+ end
115
+
116
+ def outgoing_targets(bb)
117
+ outgoing_edges(bb).map { |edge| edge.target }
118
+ end
119
+
120
+ def incoming_sources(bb)
121
+ @graph.edges.select { |edge| edge.target == bb }.map {|edge| edge.source}
122
+ end
123
+
124
+ def new_bb(label)
125
+ @bb_map[label] = bb = BasicBlock.new(label)
126
+ @graph.add_vertex(bb)
127
+ @graph.set_vertex_options(bb, shape: 'box', style: 'rounded')
128
+ bb
129
+ end
130
+
131
+ def remove_bb(bb)
132
+ @bb_map.delete(bb.label)
133
+ @graph.remove_vertex(bb)
134
+ incoming_sources(bb).each { |other| remove_edge(other, bb) }
135
+ outgoing_targets(bb).each { |other| remove_edge(bb, other) }
136
+ end
137
+
138
+ def add_forward_edge(source_bb, target_label, forward_refs)
139
+ target_bb = @bb_map[target_label]
140
+
141
+ if target_bb
142
+ add_jump_edge(source_bb, target_bb)
143
+ else
144
+ forward_refs[target_label] ||= []
145
+ forward_refs[target_label] << source_bb
146
+ end
147
+ end
148
+
149
+ def add_edge(source_bb, target_bb, edge_type)
150
+ @edge_labels[[source_bb, target_bb]] = edge_type
151
+ @graph.add_edge(source_bb, target_bb)
152
+ @graph.set_edge_options(source_bb, target_bb, label: edge_type.to_s, color: edge_type_color(edge_type))
153
+ target_bb
154
+ end
155
+
156
+ def remove_edge(source_bb, target_bb)
157
+ @edge_labels.delete([source_bb, target_bb])
158
+ @graph.remove_edge(source_bb, target_bb)
159
+ end
160
+
161
+ def edge_type_color(edge_type) = edge_type == :jump ? 'blue' : 'green'
162
+
163
+ def add_jump_edge(source_bb, target_bb) = add_edge(source_bb, target_bb, :jump)
164
+ def add_fallthrough_edge(source_bb, target_bb) = add_edge(source_bb, target_bb, :fall_through)
165
+
166
+ def build(instrs)
167
+ forward_references = {}
168
+
169
+ current_bb = new_bb(:entry)
170
+ @entry_bb = current_bb
171
+ @exit_bb = new_bb(:exit)
172
+ @exit_bb.instrs << Instructions::LabelInstr.new(:exit)
173
+
174
+ just_jumped = false
175
+
176
+ instrs.each do |instr|
177
+ case instr
178
+ when Instructions::TwoOperandJumpInstr # :gt, :bne, :beq
179
+ just_jumped = false
180
+ current_bb.add_instr(instr)
181
+ add_forward_edge(current_bb, instr.value, forward_references)
182
+ current_bb = add_fallthrough_edge(current_bb, new_bb("fall_thru_#{instr.object_id}"))
183
+ when Instructions::ExitInstr
184
+ just_jumped = true
185
+ add_jump(current_bb, @exit_bb.label, instr)
186
+ add_jump_edge(current_bb, @exit_bb)
187
+ when Instructions::JumpInstr # :jump
188
+ current_bb.add_instr(instr)
189
+ add_forward_edge(current_bb, instr.value, forward_references)
190
+ just_jumped = true
191
+ when Instructions::LabelInstr
192
+ label = instr.value
193
+ bb = new_bb(label)
194
+ if just_jumped # jump foo\nlabel something_else\n
195
+ just_jumped = false
196
+ else
197
+ add_fallthrough_edge(current_bb, bb)
198
+ end
199
+ current_bb = bb
200
+ forward_references[label]&.each do |source_bb|
201
+ add_jump_edge(source_bb, bb)
202
+ end
203
+ current_bb.add_instr(instr)
204
+ else
205
+ just_jumped = false
206
+ current_bb.add_instr(instr)
207
+ end
208
+ end
209
+
210
+ has_edge = @graph.edges.find { |edge| edge.source == current_bb }
211
+ add_fallthrough_edge(current_bb, @exit_bb) unless has_edge
212
+
213
+ @edge_props = RGL::EdgePropertiesMap.new(@edge_labels, true)
214
+ end
215
+
216
+ def cull
217
+ #show_single_jump_bbs
218
+ cull_dead_bbs
219
+ cull_isolated_bbs
220
+ remove_empty_bbs
221
+ jump_reduction
222
+ cull_dead_bbs
223
+ cull_isolated_bbs
224
+ recombine_pntr
225
+ end
226
+
227
+ def show_single_jump_bbs
228
+ @bb_map.each_value do |bb|
229
+ puts "BB: #{bb.label}"
230
+ puts "---------------"
231
+ puts bb.instrs.map { |i| i.disasm }.join("\n")
232
+ puts
233
+ end
234
+ end
235
+
236
+ # doing up to 3 branches + 1 jump can be optimized by recombining these back
237
+ # into a pntr instruction which can just use a jump table
238
+ def recombine_pntr
239
+ pntrs = {}
240
+ @bb_map.each_value do |bb|
241
+ last_instr = bb.last_instr
242
+ #puts "BB #{bb.label} #{last_instr} #{bb.instrs.size}"
243
+ if bb.label.to_s =~ /pntr_3_(\d+)/
244
+ pntrs[$1] ||= []
245
+ pntrs[$1] << [$1, "3", bb]
246
+ next
247
+ end
248
+ if last_instr.kind_of?(Instructions::BEQInstr)
249
+ if last_instr.label.to_s =~ /pntr_(\d+)_(\d+)/
250
+ #puts "1: #{$1}, 2: #{$2}"
251
+ pntrs[$2] ||= []
252
+ pntrs[$2] << [$2, $1, bb]
253
+ end
254
+ end
255
+ end
256
+
257
+ pntrs.each_value.sort { |(step, i, _), (step2, i2, _)| step <=> step2 && i <=> i2 }
258
+
259
+ pntrs.each_value do |list|
260
+ new_instr = Instructions::PntrInstr.new(list[0][0])
261
+ zero_bb = nil
262
+ list.each.each_with_index do |(step, _, bb), i|
263
+ if i != 3
264
+ label = bb.instrs.last.label
265
+ if i == 0
266
+ compare = bb.instrs.last.operand1
267
+ new_instr.operands << compare
268
+ bb.instrs[-1] = new_instr
269
+ zero_bb = bb
270
+ else
271
+ add_edge(zero_bb, outgoing_target(bb, :jump), :jump)
272
+ remove_bb(bb)
273
+ end
274
+ new_instr.operands << label
275
+ else
276
+ if bb.only_contains_jump?
277
+ jump_bb = outgoing_target(bb, :jump)
278
+ remove_bb(bb)
279
+ bb = jump_bb
280
+ end
281
+ add_edge(zero_bb, bb, :jump)
282
+ new_instr.operands << bb.label
283
+ end
284
+ end
285
+ end
286
+ end
287
+
288
+ def jump_reduction
289
+ @bb_map.each_value do |bb|
290
+ #puts "BB: #{bb.label}"
291
+ if @graph.out_degree(bb) == 1
292
+ next_bb = outgoing_target(bb, :fall_through)
293
+
294
+ if next_bb && @graph.out_degree(next_bb) == 1 && next_bb.only_contains_jump?
295
+ #puts "JUMP REPLACE: #{bb.label} -> #{next_bb.label}"
296
+ jump = next_bb.instrs.last
297
+ if bb.instrs[-1].kind_of?(Instructions::JumpInstr)
298
+ bb.instrs[-1] = jump
299
+ else
300
+ bb.instrs << jump
301
+ end
302
+ remove_edge(bb, next_bb)
303
+ add_edge(bb, outgoing_target(next_bb, :jump), :jump)
304
+ next
305
+ end
306
+
307
+ next_bb = outgoing_target(bb, :jump)
308
+ if next_bb && @graph.out_degree(next_bb) == 1 && next_bb.only_contains_jump?
309
+ #puts "JUMP REPLACE: #{bb.label} -> #{next_bb.label}"
310
+ jump = next_bb.instrs.last
311
+ bb.instrs[-1] = jump
312
+ remove_edge(bb, next_bb)
313
+ add_edge(bb, outgoing_target(next_bb, :jump), :jump)
314
+ next
315
+ end
316
+ end
317
+ end
318
+ end
319
+
320
+ def remove_empty_bbs
321
+ # This is a bit wonky but pntr[3] needs fall through to jump to body for pntr[3]
322
+ # I do not think the pattern can occur any other way so I am assuming a lot in this
323
+ # method about its structure. For CFG interp this has no impact but for displaying
324
+ # The CFG itself it removes an empty bb. It also makes recombining the pntr to
325
+ # a jump table simpler.
326
+ @bb_map.each_value do |bb|
327
+ if @graph.out_degree(bb) == 1 && bb.instrs.empty?
328
+ target = outgoing_target(bb, :fall_through)
329
+ source = incoming_sources(bb).first
330
+ remove_bb(bb)
331
+ add_edge(source, target, :fall_through)
332
+ end
333
+ end
334
+ end
335
+
336
+ def cull_dead_bbs
337
+ @bb_map.each_value do |bb|
338
+ jump_bb = outgoing_target(bb, :jump)
339
+
340
+ # How we make CFG will only have a fall_through edge if next bb has an incoming edge
341
+ # AND/OR if we have a jump. We do not act on the incoming edge so we only cull if we
342
+ # see a constant jump
343
+ next unless jump_bb
344
+
345
+ fallthrough_bb = outgoing_target(bb, :fall_through)
346
+ last_instr = bb.instrs.last
347
+ if last_instr&.constant? # We can elminate an edge
348
+ result = last_instr.execute(nil)
349
+ if result
350
+ remove_edge(bb, fallthrough_bb)
351
+ else
352
+ remove_edge(bb, jump_bb)
353
+ puts "DELETING: #{bb.label}:#{last_instr}"
354
+ bb.instrs.delete(last_instr)
355
+ end
356
+ end
357
+ end
358
+ end
359
+
360
+ def cull_isolated_bbs
361
+ @bb_map.each do |label, bb|
362
+ next if bb == @entry_bb
363
+
364
+ in_degree = incoming_sources(bb).size
365
+ if in_degree == 0 # If we cannot make it to this bb delete itself and its outgoing edged
366
+ remove_bb(bb)
367
+ end
368
+ end
369
+ end
370
+
371
+ def combine_bbs(bb)
372
+ other_bb = outgoing_targets(bb)[0]
373
+ puts "combining #{bb.label} with #{other_bb.label}" if debug
374
+ bb.instrs.concat(other_bb.instrs)
375
+ remove_edge(bb, other_bb)
376
+ replace_edge(bb, other_bb, :fall_through)
377
+ replace_edge(bb, other_bb, :jump)
378
+ end
379
+
380
+ def replace_edge(bb, other_bb, edge_type)
381
+ other_edge = outgoing_edges(other_bb, edge_type)
382
+ if other_edge
383
+ remove_edge(edge.source, edge.target)
384
+ add_edge(bb, edge.target, edge_type)
385
+ end
386
+ end
387
+
388
+ # Linearization rearranges basic blocks around fall throughs all being sequentially after
389
+ # the previous fall through bb. All bbs jumped to will get put after that.
390
+ #
391
+ # For these moved blocks we need to add a jump to the bb where execution continues from
392
+ # that bb unless the next bb happens to be right after it.
393
+ #
394
+ # The opposite case is true where last instr is a jump but the jump location happens to
395
+ # be the next bb. In that case we remove the jump.
396
+ #
397
+ # Piet only has two edge types (fall_through, jump) which makes this simpler than
398
+ # other languages which may return (from procedures/functions) or may raise exceptions.
399
+ def linearize
400
+ sorted_list, visited = [], {@exit_bb => true}
401
+ linearize_inner(sorted_list, visited, @entry_bb)
402
+ sorted_list << @exit_bb
403
+ recalculate_jumps(sorted_list)
404
+ sorted_list
405
+ end
406
+
407
+ def postorder_bbs
408
+ order = []
409
+ @graph.depth_first_visit(@entry_bb) { |v| order << v }
410
+ order
411
+ end
412
+
413
+ def postorder_bbs2
414
+ result, work_list, visited = [], [@entry_bb], {@entry_bb => true}
415
+
416
+ while !work_list.empty?
417
+ bb = work_list.last
418
+ all_children_visited = true
419
+ outgoing_targets(bb).each do |dest|
420
+ next if visited[dest]
421
+ visited[dest] = true
422
+ all_children_visited = false
423
+ if @graph.out_degree(dest) == 0 # should only be exit bb
424
+ result << dest
425
+ else
426
+ work_list << dest
427
+ end
428
+ end
429
+
430
+ if all_children_visited
431
+ work_list.pop
432
+ result << bb
433
+ end
434
+ end
435
+
436
+ result
437
+ end
438
+
439
+ def preorder_bbs
440
+ postorder_bbs.reverse
441
+ end
442
+
443
+ private
444
+
445
+ def linearize_inner(sorted_list, visited, bb)
446
+ return if visited[bb]
447
+ visited[bb] = true
448
+ sorted_list << bb
449
+
450
+ fall_through = outgoing_target(bb, :fall_through)
451
+ linearize_inner(sorted_list, visited, fall_through) if fall_through
452
+ outgoing_targets(bb).each do |jump|
453
+ next if jump == fall_through
454
+ linearize_inner(sorted_list, visited, jump) if jump
455
+ end
456
+ end
457
+
458
+ def outgoing_edges_match?(bb, edge, edge_type)
459
+ edge.source == bb && (!edge_type || @edge_props.edge_property(edge.source, edge.target) == edge_type)
460
+ end
461
+
462
+ def check_for_unneeded_jump(current_bb, next_bb, jump)
463
+ if jump.label == next_bb.label
464
+ remove_edge(current_bb, next_bb)
465
+ add_edge(current_bb, next_bb, :fall_through)
466
+ current_bb.instrs.pop
467
+ end
468
+ end
469
+
470
+ def check_for_needed_jump(current_bb, next_bb, last_instr)
471
+ return if current_bb == exit_bb
472
+ dest = outgoing_target(current_bb, :fall_through)
473
+ add_jump(current_bb, dest.label, last_instr) if dest && dest != next_bb
474
+ end
475
+
476
+ def add_jump(bb, label, last_instr)
477
+ jump = Instructions::JumpInstr.new(label)
478
+ jump.graph_node = last_instr&.graph_node
479
+ bb.instrs << jump
480
+ end
481
+
482
+ def recalculate_jumps(bbs)
483
+ bbs.each_with_index do |bb, index|
484
+ last_instr = bb.instrs.last
485
+ if last_instr&.operation == :jump
486
+ check_for_unneeded_jump(bb, bbs[index + 1], last_instr)
487
+ else
488
+ check_for_needed_jump(bb, bbs[index + 1], last_instr)
489
+ end
490
+ end
491
+ end
492
+ end
493
+ end
494
+ end