seafoam 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 (147) hide show
  1. checksums.yaml +7 -0
  2. data/.github/probots.yml +2 -0
  3. data/.github/workflows/rubocop.yml +10 -0
  4. data/.github/workflows/specs.yml +19 -0
  5. data/.gitignore +7 -0
  6. data/.rubocop.yml +34 -0
  7. data/.ruby-version +1 -0
  8. data/.seafoam/config +1 -0
  9. data/CODE_OF_CONDUCT.md +128 -0
  10. data/CONTRIBUTING.md +5 -0
  11. data/Gemfile +2 -0
  12. data/LICENSE.md +7 -0
  13. data/README.md +298 -0
  14. data/bin/bgv2isabelle +53 -0
  15. data/bin/bgv2json +42 -0
  16. data/bin/seafoam +24 -0
  17. data/docs/annotators.md +43 -0
  18. data/docs/bgv.md +284 -0
  19. data/docs/getting-graphs.md +47 -0
  20. data/examples/Fib.java +24 -0
  21. data/examples/MatMult.java +39 -0
  22. data/examples/fib-java.bgv +0 -0
  23. data/examples/fib-js.bgv +0 -0
  24. data/examples/fib-ruby.bgv +0 -0
  25. data/examples/fib.js +15 -0
  26. data/examples/fib.rb +15 -0
  27. data/examples/identity.bgv +0 -0
  28. data/examples/identity.rb +13 -0
  29. data/examples/java/Irreducible.j +35 -0
  30. data/examples/java/IrreducibleDecompiled.java +21 -0
  31. data/examples/java/JavaExamples.java +418 -0
  32. data/examples/java/exampleArithOperator.bgv +0 -0
  33. data/examples/java/exampleArithOperator.cfg +925 -0
  34. data/examples/java/exampleArrayAllocation.bgv +0 -0
  35. data/examples/java/exampleArrayAllocation.cfg +5268 -0
  36. data/examples/java/exampleArrayRead.bgv +0 -0
  37. data/examples/java/exampleArrayRead.cfg +2263 -0
  38. data/examples/java/exampleArrayWrite.bgv +0 -0
  39. data/examples/java/exampleArrayWrite.cfg +2315 -0
  40. data/examples/java/exampleCatch.bgv +0 -0
  41. data/examples/java/exampleCatch.cfg +4150 -0
  42. data/examples/java/exampleCompareOperator.bgv +0 -0
  43. data/examples/java/exampleCompareOperator.cfg +1109 -0
  44. data/examples/java/exampleDoubleSynchronized.bgv +0 -0
  45. data/examples/java/exampleDoubleSynchronized.cfg +26497 -0
  46. data/examples/java/exampleExactArith.bgv +0 -0
  47. data/examples/java/exampleExactArith.cfg +1888 -0
  48. data/examples/java/exampleFieldRead.bgv +0 -0
  49. data/examples/java/exampleFieldRead.cfg +1228 -0
  50. data/examples/java/exampleFieldWrite.bgv +0 -0
  51. data/examples/java/exampleFieldWrite.cfg +1102 -0
  52. data/examples/java/exampleFor.bgv +0 -0
  53. data/examples/java/exampleFor.cfg +3936 -0
  54. data/examples/java/exampleFullEscape.bgv +0 -0
  55. data/examples/java/exampleFullEscape.cfg +5893 -0
  56. data/examples/java/exampleIf.bgv +0 -0
  57. data/examples/java/exampleIf.cfg +2462 -0
  58. data/examples/java/exampleIfNeverTaken.bgv +0 -0
  59. data/examples/java/exampleIfNeverTaken.cfg +2476 -0
  60. data/examples/java/exampleInstanceOfManyImpls.bgv +0 -0
  61. data/examples/java/exampleInstanceOfManyImpls.cfg +6391 -0
  62. data/examples/java/exampleInstanceOfOneImpl.bgv +0 -0
  63. data/examples/java/exampleInstanceOfOneImpl.cfg +2604 -0
  64. data/examples/java/exampleIntSwitch.bgv +0 -0
  65. data/examples/java/exampleIntSwitch.cfg +3121 -0
  66. data/examples/java/exampleInterfaceCallManyImpls.bgv +0 -0
  67. data/examples/java/exampleInterfaceCallManyImpls.cfg +1358 -0
  68. data/examples/java/exampleInterfaceCallOneImpl.bgv +0 -0
  69. data/examples/java/exampleInterfaceCallOneImpl.cfg +3859 -0
  70. data/examples/java/exampleLocalInstanceOf.bgv +0 -0
  71. data/examples/java/exampleLocalInstanceOf.cfg +5276 -0
  72. data/examples/java/exampleLocalSynchronized.bgv +0 -0
  73. data/examples/java/exampleLocalSynchronized.cfg +1364 -0
  74. data/examples/java/exampleLocalVariables.bgv +0 -0
  75. data/examples/java/exampleLocalVariables.cfg +1195 -0
  76. data/examples/java/exampleLocalVariablesState.bgv +0 -0
  77. data/examples/java/exampleLocalVariablesState.cfg +1673 -0
  78. data/examples/java/exampleNestedWhile.bgv +0 -0
  79. data/examples/java/exampleNestedWhile.cfg +15499 -0
  80. data/examples/java/exampleNestedWhileBreak.bgv +0 -0
  81. data/examples/java/exampleNestedWhileBreak.cfg +11162 -0
  82. data/examples/java/exampleNoEscape.bgv +0 -0
  83. data/examples/java/exampleNoEscape.cfg +974 -0
  84. data/examples/java/exampleObjectAllocation.bgv +0 -0
  85. data/examples/java/exampleObjectAllocation.cfg +5287 -0
  86. data/examples/java/examplePartialEscape.bgv +0 -0
  87. data/examples/java/examplePartialEscape.cfg +7042 -0
  88. data/examples/java/examplePhi.bgv +0 -0
  89. data/examples/java/examplePhi.cfg +3227 -0
  90. data/examples/java/exampleReducible.bgv +0 -0
  91. data/examples/java/exampleReducible.cfg +5578 -0
  92. data/examples/java/exampleSimpleCall.bgv +0 -0
  93. data/examples/java/exampleSimpleCall.cfg +1435 -0
  94. data/examples/java/exampleStamp.bgv +0 -0
  95. data/examples/java/exampleStamp.cfg +913 -0
  96. data/examples/java/exampleStaticCall.bgv +0 -0
  97. data/examples/java/exampleStaticCall.cfg +1154 -0
  98. data/examples/java/exampleStringSwitch.bgv +0 -0
  99. data/examples/java/exampleStringSwitch.cfg +15377 -0
  100. data/examples/java/exampleSynchronized.bgv +0 -0
  101. data/examples/java/exampleSynchronized.cfg +26027 -0
  102. data/examples/java/exampleThrow.bgv +0 -0
  103. data/examples/java/exampleThrow.cfg +780 -0
  104. data/examples/java/exampleThrowCatch.bgv +0 -0
  105. data/examples/java/exampleThrowCatch.cfg +744 -0
  106. data/examples/java/exampleUnsafeRead.bgv +0 -0
  107. data/examples/java/exampleUnsafeRead.cfg +912 -0
  108. data/examples/java/exampleUnsafeWrite.bgv +0 -0
  109. data/examples/java/exampleUnsafeWrite.cfg +962 -0
  110. data/examples/java/exampleWhile.bgv +0 -0
  111. data/examples/java/exampleWhile.cfg +3936 -0
  112. data/examples/java/exampleWhileBreak.bgv +0 -0
  113. data/examples/java/exampleWhileBreak.cfg +5963 -0
  114. data/examples/matmult-java.bgv +0 -0
  115. data/examples/matmult-ruby.bgv +0 -0
  116. data/examples/matmult.rb +29 -0
  117. data/examples/overflow.bgv +0 -0
  118. data/examples/overflow.rb +13 -0
  119. data/lib/seafoam.rb +13 -0
  120. data/lib/seafoam/annotators.rb +54 -0
  121. data/lib/seafoam/annotators/fallback.rb +27 -0
  122. data/lib/seafoam/annotators/graal.rb +376 -0
  123. data/lib/seafoam/bgv/bgv_parser.rb +602 -0
  124. data/lib/seafoam/binary/binary_reader.rb +21 -0
  125. data/lib/seafoam/binary/io_binary_reader.rb +88 -0
  126. data/lib/seafoam/colors.rb +18 -0
  127. data/lib/seafoam/commands.rb +447 -0
  128. data/lib/seafoam/config.rb +34 -0
  129. data/lib/seafoam/graph.rb +91 -0
  130. data/lib/seafoam/graphviz_writer.rb +213 -0
  131. data/lib/seafoam/spotlight.rb +28 -0
  132. data/lib/seafoam/version.rb +5 -0
  133. data/seafoam.gemspec +20 -0
  134. data/spec/seafoam/annotators/fallback_spec.rb +69 -0
  135. data/spec/seafoam/annotators/graal_spec.rb +96 -0
  136. data/spec/seafoam/annotators_spec.rb +61 -0
  137. data/spec/seafoam/bgv/bgv_parser_spec.rb +144 -0
  138. data/spec/seafoam/bgv/fixtures/not.bgv +1 -0
  139. data/spec/seafoam/bgv/fixtures/unsupported.bgv +1 -0
  140. data/spec/seafoam/binary/io_binary_reader_spec.rb +176 -0
  141. data/spec/seafoam/command_spec.rb +252 -0
  142. data/spec/seafoam/graph_spec.rb +172 -0
  143. data/spec/seafoam/graphviz_writer_spec.rb +63 -0
  144. data/spec/seafoam/spec_helpers.rb +30 -0
  145. data/spec/seafoam/spotlight_spec.rb +38 -0
  146. data/tools/render-all +36 -0
  147. metadata +238 -0
@@ -0,0 +1,21 @@
1
+ require 'stringio'
2
+
3
+ module Seafoam
4
+ module Binary
5
+ # Factory for binary readers based on the input.
6
+ module BinaryReader
7
+ def self.for(source)
8
+ case source
9
+ when File
10
+ IOBinaryReader.new(StringIO.new(File.read(source)))
11
+ when IO
12
+ IOBinaryReader.new(source)
13
+ when String
14
+ IOBinaryReader.new(StringIO.new(source))
15
+ else
16
+ raise source.class
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,88 @@
1
+ module Seafoam
2
+ module Binary
3
+ # An adapter to read binary values from an IO stream.
4
+ class IOBinaryReader
5
+ def initialize(io)
6
+ @io = io
7
+ end
8
+
9
+ def read_utf8(length)
10
+ read_bytes(length).force_encoding Encoding::UTF_8
11
+ end
12
+
13
+ def read_bytes(length)
14
+ @io.read(length)
15
+ end
16
+
17
+ def read_float64
18
+ @io.read(8).unpack1('G')
19
+ end
20
+
21
+ def read_float32
22
+ @io.read(4).unpack1('g')
23
+ end
24
+
25
+ def read_sint64
26
+ @io.read(8).unpack1('q>')
27
+ end
28
+
29
+ def read_sint32
30
+ @io.read(4).unpack1('l>')
31
+ end
32
+
33
+ def read_sint16
34
+ @io.read(2).unpack1('s>')
35
+ end
36
+
37
+ def read_uint16
38
+ @io.read(2).unpack1('S>')
39
+ end
40
+
41
+ def read_sint8
42
+ @io.read(1).unpack1('c')
43
+ end
44
+
45
+ def read_uint8
46
+ @io.readbyte
47
+ end
48
+
49
+ def peek_sint8
50
+ byte = @io.read(1).unpack1('c')
51
+ @io.ungetbyte byte
52
+ byte
53
+ end
54
+
55
+ def skip_float64(count = 1)
56
+ skip count * 8
57
+ end
58
+
59
+ def skip_float32(count = 1)
60
+ skip count * 4
61
+ end
62
+
63
+ def skip_int64(count = 1)
64
+ skip count * 8
65
+ end
66
+
67
+ def skip_int32(count = 1)
68
+ skip count * 4
69
+ end
70
+
71
+ def skip_int16(count = 1)
72
+ skip count * 2
73
+ end
74
+
75
+ def skip_int8(count = 1)
76
+ skip count
77
+ end
78
+
79
+ def skip(count)
80
+ @io.seek count, IO::SEEK_CUR
81
+ end
82
+
83
+ def eof?
84
+ @io.eof?
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,18 @@
1
+ module Seafoam
2
+ # Colors from and derived from the TruffleRuby logo.
3
+
4
+ # TruffleRuby logo Copyright (C) 2017 Talkdesk, Inc.
5
+ # Licensed under CC BY 4.0: https://creativecommons.org/licenses/by/4.0/
6
+
7
+ WHITE_ICE = '#d7ede7'
8
+ CRUISE = '#b8ddd1'
9
+ KEPPEL = '#3cb4a4'
10
+ CARISSMA = '#e98693'
11
+ AMARANTH = '#da2d4f'
12
+ BLACK = '#1a1919'
13
+ WHITE = '#ffffff'
14
+ DUST = '#f9f9f9'
15
+ BIG_STONE = '#343d46'
16
+ ICE_STONE = '#b3bbc3'
17
+ ORANGE = '#ffa500'
18
+ end
@@ -0,0 +1,447 @@
1
+ require 'json'
2
+
3
+ module Seafoam
4
+ # Implementations of the command-line commands that you can run in Seafoam.
5
+ class Commands
6
+ def initialize(out, config)
7
+ @out = out
8
+ @config = config
9
+ end
10
+
11
+ # Run any command.
12
+ def run(*args)
13
+ first, *args = args
14
+ case first
15
+ when nil, 'help', '-h', '--help', '-help'
16
+ help(*args)
17
+ when 'version', '-v', '-version', '--version'
18
+ version(*args)
19
+ else
20
+ name = first
21
+ command, *args = args
22
+ case command
23
+ when nil
24
+ help(*args)
25
+ when 'info'
26
+ info name, *args
27
+ when 'list'
28
+ list name, *args
29
+ when 'search'
30
+ search name, *args
31
+ when 'edges'
32
+ edges name, *args
33
+ when 'props'
34
+ props name, *args
35
+ when 'render'
36
+ render name, *args
37
+ when 'debug'
38
+ debug name, *args
39
+ else
40
+ raise ArgumentError, "unknown command #{command}"
41
+ end
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ # seafoam file.bgv info
48
+ def info(name, *args)
49
+ file, *rest = parse_name(name)
50
+ raise ArgumentError, 'info only works with a file' unless rest == [nil, nil, nil]
51
+
52
+ raise ArgumentError, 'info does not take arguments' unless args.empty?
53
+
54
+ parser = BGV::BGVParser.new(File.new(file))
55
+ major, minor = parser.read_file_header(version_check: false)
56
+ @out.puts "BGV #{major}.#{minor}"
57
+ end
58
+
59
+ # seafoam file.bgv list
60
+ def list(name, *args)
61
+ file, *rest = parse_name(name)
62
+ raise ArgumentError, 'list only works with a file' unless rest == [nil, nil, nil]
63
+
64
+ raise ArgumentError, 'list does not take arguments' unless args.empty?
65
+
66
+ parser = BGV::BGVParser.new(File.new(file))
67
+ parser.read_file_header
68
+ parser.skip_document_props
69
+ loop do
70
+ index, = parser.read_graph_preheader
71
+ break unless index
72
+
73
+ graph_header = parser.read_graph_header
74
+ @out.puts "#{file}:#{index} #{parser.graph_name(graph_header)}"
75
+ parser.skip_graph
76
+ end
77
+ end
78
+
79
+ # seafoam file.bgv:n... search term...
80
+ def search(name, *terms)
81
+ file, graph_index, node_id, = parse_name(name)
82
+ raise ArgumentError, 'search only works with a file or graph' if node_id
83
+
84
+ parser = BGV::BGVParser.new(File.new(file))
85
+ parser.read_file_header
86
+ parser.skip_document_props
87
+ loop do
88
+ index, = parser.read_graph_preheader
89
+ break unless index
90
+
91
+ if !graph_index || index == graph_index
92
+ header = parser.read_graph_header
93
+ search_object "#{file}:#{index}", header, terms
94
+ graph = parser.read_graph
95
+ graph.nodes.each_value do |node|
96
+ search_object "#{file}:#{index}:#{node.id}", node.props, terms
97
+ end
98
+ graph.edges.each do |edge|
99
+ search_object "#{file}:#{index}:#{edge.from.id}-#{edge.to.id}", edge.props, terms
100
+ end
101
+ else
102
+ parser.skip_graph_header
103
+ parser.skip_graph
104
+ end
105
+ end
106
+ end
107
+
108
+ def search_object(tag, object, terms)
109
+ full_text = JSON.generate(object)
110
+ full_text_down = full_text.downcase
111
+ start = 0
112
+ terms.each do |t|
113
+ loop do
114
+ index = full_text_down.index(t.downcase, start)
115
+ break unless index
116
+
117
+ context = 40
118
+ before = full_text[index - context, context]
119
+ match = full_text[index, t.size]
120
+ after = full_text[index + t.size, context]
121
+ if @out.tty?
122
+ highlight_on = "\033[1m"
123
+ highlight_off = "\033[0m"
124
+ else
125
+ highlight_on = ''
126
+ highlight_off = ''
127
+ end
128
+ @out.puts "#{tag} ...#{before}#{highlight_on}#{match}#{highlight_off}#{after}..."
129
+ start = index + t.size
130
+ end
131
+ end
132
+ end
133
+
134
+ # seafoam file.bgv:n... edges
135
+ def edges(name, *args)
136
+ file, graph_index, node_id, edge_id = parse_name(name)
137
+ raise ArgumentError, 'edges needs at least a graph' unless graph_index
138
+
139
+ raise ArgumentError, 'edges does not take arguments' unless args.empty?
140
+
141
+ with_graph(file, graph_index) do |parser|
142
+ parser.read_graph_header
143
+ graph = parser.read_graph
144
+ if node_id
145
+ Annotators.apply graph
146
+ node = graph.nodes[node_id]
147
+ raise ArgumentError, 'node not found' unless node
148
+
149
+ if edge_id
150
+ to = graph.nodes[edge_id]
151
+ raise ArgumentError, 'edge node not found' unless to
152
+
153
+ edges = node.outputs.select { |edge| edge.to == to }
154
+ raise ArgumentError, 'edge not found' if edges.empty?
155
+
156
+ edges.each do |edge|
157
+ @out.puts "#{edge.from.id_and_label} ->(#{edge.props[:label]}) #{edge.to.id_and_label}"
158
+ end
159
+ else
160
+ @out.puts 'Input:'
161
+ node.inputs.each do |input|
162
+ @out.puts " #{node.id_and_label} <-(#{input.props[:label]}) #{input.from.id_and_label}"
163
+ end
164
+ @out.puts 'Output:'
165
+ node.outputs.each do |output|
166
+ @out.puts " #{node.id_and_label} ->(#{output.props[:label]}) #{output.to.id_and_label}"
167
+ end
168
+ end
169
+ break
170
+ else
171
+ @out.puts "#{graph.nodes.count} nodes, #{graph.edges.count} edges"
172
+ end
173
+ end
174
+ end
175
+
176
+ # seafoam file.bgv... props
177
+ def props(name, *args)
178
+ file, graph_index, node_id, edge_id = parse_name(name)
179
+ raise ArgumentError, 'props does not take arguments' unless args.empty?
180
+
181
+ if graph_index
182
+ with_graph(file, graph_index) do |parser|
183
+ graph_header = parser.read_graph_header
184
+ if node_id
185
+ graph = parser.read_graph
186
+ node = graph.nodes[node_id]
187
+ raise ArgumentError, 'node not found' unless node
188
+
189
+ if edge_id
190
+ to = graph.nodes[edge_id]
191
+ raise ArgumentError, 'edge node not found' unless to
192
+
193
+ edges = node.outputs.select { |edge| edge.to == to }
194
+ raise ArgumentError, 'edge not found' if edges.empty?
195
+
196
+ if edges.size > 1
197
+ edges.each do |edge|
198
+ pretty_print edge.props
199
+ @out.puts
200
+ end
201
+ else
202
+ pretty_print edges.first.props
203
+ end
204
+ else
205
+ pretty_print node.props
206
+ end
207
+ break
208
+ else
209
+ pretty_print graph_header
210
+ parser.skip_graph
211
+ end
212
+ end
213
+ else
214
+ parser = BGV::BGVParser.new(File.new(file))
215
+ parser.read_file_header
216
+ document_props = parser.read_document_props
217
+ pretty_print document_props || {}
218
+ end
219
+ end
220
+
221
+ # seafoam file.bgv:0 render options...
222
+ def render(name, *args)
223
+ file, graph_index, *rest = parse_name(name)
224
+ raise ArgumentError, 'render needs at least a graph' unless graph_index
225
+ raise ArgumentError, 'render only works with a graph' unless rest == [nil, nil]
226
+
227
+ annotator_options = {
228
+ hide_frame_state: true,
229
+ hide_floating: false,
230
+ reduce_edges: true
231
+ }
232
+ spotlight_nodes = nil
233
+ args = args.dup
234
+ out_file = nil
235
+ explicit_out_file = false
236
+ until args.empty?
237
+ arg = args.shift
238
+ case arg
239
+ when '--out'
240
+ out_file = args.shift
241
+ explicit_out_file = true
242
+ raise ArgumentError, 'no file for --out' unless out_file
243
+ when '--spotlight'
244
+ spotlight_arg = args.shift
245
+ raise ArgumentError, 'no list for --spotlight' unless spotlight_arg
246
+
247
+ spotlight_nodes = spotlight_arg.split(',').map { |n| Integer(n) }
248
+ when '--show-frame-state'
249
+ annotator_options[:hide_frame_state] = false
250
+ when '--hide-floating'
251
+ annotator_options[:hide_floating] = true
252
+ when '--no-reduce-edges'
253
+ annotator_options[:reduce_edges] = false
254
+ when '--option'
255
+ key = args.shift
256
+ raise ArgumentError, 'no key for --option' unless key
257
+
258
+ value = args.shift
259
+ raise ArgumentError, "no value for --option #{key}" unless out_file
260
+
261
+ value = { 'true' => true, 'false' => 'false' }.fetch(key, value)
262
+ annotator_options[key.to_sym] = value
263
+ else
264
+ raise ArgumentError, "unexpected option #{arg}"
265
+ end
266
+ end
267
+ out_file ||= 'graph.pdf'
268
+ out_ext = File.extname(out_file).downcase
269
+ case out_ext
270
+ when '.pdf'
271
+ out_format = :pdf
272
+ when '.svg'
273
+ out_format = :svg
274
+ when '.png'
275
+ out_format = :png
276
+ when '.dot'
277
+ out_format = :dot
278
+ else
279
+ raise ArgumentError, "unknown render format #{out_ext}"
280
+ end
281
+
282
+ with_graph(file, graph_index) do |parser|
283
+ parser.skip_graph_header
284
+ graph = parser.read_graph
285
+ Annotators.apply graph, annotator_options
286
+ if spotlight_nodes
287
+ spotlight = Spotlight.new(graph)
288
+ spotlight_nodes.each do |node_id|
289
+ node = graph.nodes[node_id]
290
+ raise ArgumentError, 'node not found' unless node
291
+
292
+ spotlight.light node
293
+ end
294
+ spotlight.shade
295
+ end
296
+ if out_format == :dot
297
+ File.open(out_file, 'w') do |stream|
298
+ writer = GraphvizWriter.new(stream)
299
+ writer.write_graph graph
300
+ end
301
+ else
302
+ IO.popen(['dot', "-T#{out_format}", '-o', out_file], 'w') do |stream|
303
+ writer = GraphvizWriter.new(stream)
304
+ hidpi = out_format == :png
305
+ writer.write_graph graph, hidpi
306
+ end
307
+ autoopen out_file unless explicit_out_file
308
+ end
309
+ end
310
+ end
311
+
312
+ # seafoam file.bgv debug options...
313
+ def debug(name, *args)
314
+ file, *rest = parse_name(name)
315
+ raise ArgumentError, 'debug only works with a file' unless rest == [nil, nil, nil]
316
+
317
+ skip = false
318
+ args.each do |arg|
319
+ case arg
320
+ when '--skip'
321
+ skip = true
322
+ else
323
+ raise ArgumentError, "unexpected option #{arg}"
324
+ end
325
+ end
326
+
327
+ File.open(file) do |stream|
328
+ parser = BGVDebugParser.new(@out, stream)
329
+ begin
330
+ pretty_print parser.read_file_header
331
+ document_props = parser.read_document_props
332
+ if document_props
333
+ pretty_print document_props
334
+ end
335
+ loop do
336
+ index, id = parser.read_graph_preheader
337
+ break unless index
338
+
339
+ @out.puts "graph #{index}, id=#{id}"
340
+ if skip
341
+ parser.skip_graph_header
342
+ parser.skip_graph
343
+ else
344
+ pretty_print parser.read_graph_header
345
+ pretty_print parser.read_graph
346
+ end
347
+ end
348
+ rescue StandardError => e
349
+ @out.puts "#{e} before byte #{stream.tell}"
350
+ @out.puts e.backtrace
351
+ end
352
+ end
353
+ end
354
+
355
+ # A subclass of BGVParser which prints when pool entries are added.
356
+ class BGVDebugParser < BGV::BGVParser
357
+ def initialize(out, *args)
358
+ super(*args)
359
+ @out = out
360
+ end
361
+
362
+ def set_pool_entry(id, object)
363
+ @out.puts "pool #{id} = #{object}"
364
+ super
365
+ end
366
+ end
367
+
368
+ # Reads a file and yields just the graph requested by the index - skipping
369
+ # the rest of the file as best as possible.
370
+ def with_graph(file, graph_index)
371
+ parser = BGV::BGVParser.new(File.new(file))
372
+ parser.read_file_header
373
+ parser.skip_document_props
374
+ graph_found = false
375
+ loop do
376
+ index, = parser.read_graph_preheader
377
+ break unless index
378
+
379
+ if index == graph_index
380
+ graph_found = true
381
+ yield parser
382
+ break
383
+ else
384
+ parser.skip_graph_header
385
+ parser.skip_graph
386
+ end
387
+ end
388
+ raise ArgumentError, 'graph not found' unless graph_found
389
+ end
390
+
391
+ # Prints help.
392
+ def help(*args)
393
+ raise ArgumentError, "unexpected arguments #{args.join(' ')}" unless args.empty?
394
+
395
+ @out.puts 'seafoam file.bgv info'
396
+ @out.puts ' file.bgv list'
397
+ @out.puts ' file.bgv[:graph][:node[-edge]] search term...'
398
+ @out.puts ' file.bgv[:graph][:node[-edge]] edges'
399
+ @out.puts ' file.bgv[:graph][:node[-edge]] props'
400
+ @out.puts ' file.bgv:graph render'
401
+ @out.puts ' --spotlight n,n,n...'
402
+ @out.puts ' --out graph.pdf'
403
+ @out.puts ' graph.svg'
404
+ @out.puts ' graph.png'
405
+ @out.puts ' graph.dot'
406
+ @out.puts ' --show-frame-state'
407
+ @out.puts ' --hide-floating'
408
+ @out.puts ' --no-reduce-edges'
409
+ @out.puts ' --option key value'
410
+ end
411
+
412
+ # Prints the version.
413
+ def version(*args)
414
+ raise ArgumentError, "unexpected arguments #{args.join(' ')}" unless args.empty?
415
+
416
+ @out.puts "seafoam #{VERSION}"
417
+ end
418
+
419
+ # Parse a name like file.bgv:g:n-e to [file.bgv, g, n, e].
420
+ def parse_name(name)
421
+ *pre, file, graph, node = name.split(':')
422
+ file = [*pre, file].join(':')
423
+
424
+ if node
425
+ node, edge, *rest = node.split('-')
426
+ raise ArgumentError, "too many parts to edge name in #{name}" unless rest.empty?
427
+ else
428
+ node = nil
429
+ edge = nil
430
+ end
431
+ [file] + [graph, node, edge].map { |i| i.nil? ? nil : Integer(i) }
432
+ end
433
+
434
+ # Pretty-print a JSON-style object.
435
+ def pretty_print(props)
436
+ @out.puts JSON.pretty_generate(props)
437
+ end
438
+
439
+ # Open a file for the user if possible.
440
+ def autoopen(file)
441
+ if RUBY_PLATFORM.include?('darwin') && @out.tty?
442
+ system 'open', file
443
+ # Don't worry if it fails.
444
+ end
445
+ end
446
+ end
447
+ end