seafoam 0.2 → 0.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (134) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/workflows.yml +39 -0
  3. data/.rubocop.yml +4 -1
  4. data/README.md +107 -27
  5. data/bin/bgv2isabelle +16 -45
  6. data/bin/bgv2json +18 -36
  7. data/bin/cfg2asm +24 -0
  8. data/bin/seafoam +2 -2
  9. data/demos/box-unbox-stats +65 -0
  10. data/docs/bgv.md +16 -7
  11. data/docs/getting-graphs.md +12 -0
  12. data/docs/json.md +35 -0
  13. data/examples/fib-java.bgv.gz +0 -0
  14. data/examples/fib.js +1 -1
  15. data/examples/java/JavaExamples.java +1 -1
  16. data/examples/ruby/clamps.rb +20 -0
  17. data/examples/ruby/graal.patch +15 -0
  18. data/examples/ruby/ruby_examples.rb +278 -0
  19. data/lib/seafoam.rb +5 -1
  20. data/lib/seafoam/annotators/graal.rb +1 -1
  21. data/lib/seafoam/bgv/bgv_parser.rb +12 -2
  22. data/lib/seafoam/cfg/cfg_parser.rb +93 -0
  23. data/lib/seafoam/cfg/disassembler.rb +70 -0
  24. data/lib/seafoam/commands.rb +196 -36
  25. data/lib/seafoam/graal/source.rb +23 -0
  26. data/lib/seafoam/isabelle_writer.rb +46 -0
  27. data/lib/seafoam/json_writer.rb +58 -0
  28. data/lib/seafoam/version.rb +1 -1
  29. data/seafoam.gemspec +4 -2
  30. data/spec/seafoam/annotators/graal_spec.rb +7 -7
  31. data/spec/seafoam/bgv/bgv_parser_spec.rb +27 -14
  32. data/spec/seafoam/cfg/cfg_parser_spec.rb +21 -0
  33. data/spec/seafoam/cfg/disassembler_spec.rb +32 -0
  34. data/spec/seafoam/command_spec.rb +94 -30
  35. data/spec/seafoam/graphviz_writer_spec.rb +2 -2
  36. data/spec/seafoam/json_writer_spec.rb +14 -0
  37. data/spec/seafoam/spec_helpers.rb +5 -1
  38. data/spec/seafoam/spotlight_spec.rb +1 -1
  39. data/tools/render-all +2 -2
  40. metadata +34 -98
  41. data/.github/workflows/rubocop.yml +0 -10
  42. data/.github/workflows/specs.yml +0 -19
  43. data/examples/fib-java.bgv +0 -0
  44. data/examples/fib-js.bgv +0 -0
  45. data/examples/fib-ruby.bgv +0 -0
  46. data/examples/identity.bgv +0 -0
  47. data/examples/java/exampleArithOperator.bgv +0 -0
  48. data/examples/java/exampleArithOperator.cfg +0 -925
  49. data/examples/java/exampleArrayAllocation.bgv +0 -0
  50. data/examples/java/exampleArrayAllocation.cfg +0 -5268
  51. data/examples/java/exampleArrayRead.bgv +0 -0
  52. data/examples/java/exampleArrayRead.cfg +0 -2263
  53. data/examples/java/exampleArrayWrite.bgv +0 -0
  54. data/examples/java/exampleArrayWrite.cfg +0 -2315
  55. data/examples/java/exampleCatch.bgv +0 -0
  56. data/examples/java/exampleCatch.cfg +0 -4150
  57. data/examples/java/exampleCompareOperator.bgv +0 -0
  58. data/examples/java/exampleCompareOperator.cfg +0 -1109
  59. data/examples/java/exampleDoubleSynchronized.bgv +0 -0
  60. data/examples/java/exampleDoubleSynchronized.cfg +0 -26497
  61. data/examples/java/exampleExactArith.bgv +0 -0
  62. data/examples/java/exampleExactArith.cfg +0 -1888
  63. data/examples/java/exampleFieldRead.bgv +0 -0
  64. data/examples/java/exampleFieldRead.cfg +0 -1228
  65. data/examples/java/exampleFieldWrite.bgv +0 -0
  66. data/examples/java/exampleFieldWrite.cfg +0 -1102
  67. data/examples/java/exampleFor.bgv +0 -0
  68. data/examples/java/exampleFor.cfg +0 -3936
  69. data/examples/java/exampleFullEscape.bgv +0 -0
  70. data/examples/java/exampleFullEscape.cfg +0 -5893
  71. data/examples/java/exampleIf.bgv +0 -0
  72. data/examples/java/exampleIf.cfg +0 -2462
  73. data/examples/java/exampleIfNeverTaken.bgv +0 -0
  74. data/examples/java/exampleIfNeverTaken.cfg +0 -2476
  75. data/examples/java/exampleInstanceOfManyImpls.bgv +0 -0
  76. data/examples/java/exampleInstanceOfManyImpls.cfg +0 -6391
  77. data/examples/java/exampleInstanceOfOneImpl.bgv +0 -0
  78. data/examples/java/exampleInstanceOfOneImpl.cfg +0 -2604
  79. data/examples/java/exampleIntSwitch.bgv +0 -0
  80. data/examples/java/exampleIntSwitch.cfg +0 -3121
  81. data/examples/java/exampleInterfaceCallManyImpls.bgv +0 -0
  82. data/examples/java/exampleInterfaceCallManyImpls.cfg +0 -1358
  83. data/examples/java/exampleInterfaceCallOneImpl.bgv +0 -0
  84. data/examples/java/exampleInterfaceCallOneImpl.cfg +0 -3859
  85. data/examples/java/exampleLocalInstanceOf.bgv +0 -0
  86. data/examples/java/exampleLocalInstanceOf.cfg +0 -5276
  87. data/examples/java/exampleLocalSynchronized.bgv +0 -0
  88. data/examples/java/exampleLocalSynchronized.cfg +0 -1364
  89. data/examples/java/exampleLocalVariables.bgv +0 -0
  90. data/examples/java/exampleLocalVariables.cfg +0 -1195
  91. data/examples/java/exampleLocalVariablesState.bgv +0 -0
  92. data/examples/java/exampleLocalVariablesState.cfg +0 -1673
  93. data/examples/java/exampleNestedWhile.bgv +0 -0
  94. data/examples/java/exampleNestedWhile.cfg +0 -15499
  95. data/examples/java/exampleNestedWhileBreak.bgv +0 -0
  96. data/examples/java/exampleNestedWhileBreak.cfg +0 -11162
  97. data/examples/java/exampleNoEscape.bgv +0 -0
  98. data/examples/java/exampleNoEscape.cfg +0 -974
  99. data/examples/java/exampleObjectAllocation.bgv +0 -0
  100. data/examples/java/exampleObjectAllocation.cfg +0 -5287
  101. data/examples/java/examplePartialEscape.bgv +0 -0
  102. data/examples/java/examplePartialEscape.cfg +0 -7042
  103. data/examples/java/examplePhi.bgv +0 -0
  104. data/examples/java/examplePhi.cfg +0 -3227
  105. data/examples/java/exampleReducible.bgv +0 -0
  106. data/examples/java/exampleReducible.cfg +0 -5578
  107. data/examples/java/exampleSimpleCall.bgv +0 -0
  108. data/examples/java/exampleSimpleCall.cfg +0 -1435
  109. data/examples/java/exampleStamp.bgv +0 -0
  110. data/examples/java/exampleStamp.cfg +0 -913
  111. data/examples/java/exampleStaticCall.bgv +0 -0
  112. data/examples/java/exampleStaticCall.cfg +0 -1154
  113. data/examples/java/exampleStringSwitch.bgv +0 -0
  114. data/examples/java/exampleStringSwitch.cfg +0 -15377
  115. data/examples/java/exampleSynchronized.bgv +0 -0
  116. data/examples/java/exampleSynchronized.cfg +0 -26027
  117. data/examples/java/exampleThrow.bgv +0 -0
  118. data/examples/java/exampleThrow.cfg +0 -780
  119. data/examples/java/exampleThrowCatch.bgv +0 -0
  120. data/examples/java/exampleThrowCatch.cfg +0 -744
  121. data/examples/java/exampleUnsafeRead.bgv +0 -0
  122. data/examples/java/exampleUnsafeRead.cfg +0 -912
  123. data/examples/java/exampleUnsafeWrite.bgv +0 -0
  124. data/examples/java/exampleUnsafeWrite.cfg +0 -962
  125. data/examples/java/exampleWhile.bgv +0 -0
  126. data/examples/java/exampleWhile.cfg +0 -3936
  127. data/examples/java/exampleWhileBreak.bgv +0 -0
  128. data/examples/java/exampleWhileBreak.cfg +0 -5963
  129. data/examples/matmult-java.bgv +0 -0
  130. data/examples/matmult-ruby.bgv +0 -0
  131. data/examples/overflow.bgv +0 -0
  132. data/lib/seafoam/binary/binary_reader.rb +0 -21
  133. data/spec/seafoam/bgv/fixtures/not.bgv +0 -1
  134. data/spec/seafoam/bgv/fixtures/unsupported.bgv +0 -1
data/lib/seafoam.rb CHANGED
@@ -1,13 +1,17 @@
1
1
  require 'seafoam/version'
2
2
  require 'seafoam/binary/io_binary_reader'
3
- require 'seafoam/binary/binary_reader'
4
3
  require 'seafoam/bgv/bgv_parser'
4
+ require 'seafoam/cfg/cfg_parser'
5
+ require 'seafoam/cfg/disassembler'
5
6
  require 'seafoam/colors'
6
7
  require 'seafoam/graph'
8
+ require 'seafoam/graal/source'
7
9
  require 'seafoam/annotators'
8
10
  require 'seafoam/annotators/graal'
9
11
  require 'seafoam/annotators/fallback'
10
12
  require 'seafoam/spotlight'
13
+ require 'seafoam/isabelle_writer'
14
+ require 'seafoam/json_writer'
11
15
  require 'seafoam/graphviz_writer'
12
16
  require 'seafoam/config'
13
17
  require 'seafoam/commands'
@@ -357,7 +357,7 @@ module Seafoam
357
357
  TRIGGERS = %w[
358
358
  HostedGraphBuilderPhase
359
359
  GraalCompiler
360
- TruffleCompilerThread
360
+ TruffleCompiler
361
361
  ]
362
362
 
363
363
  # Simple input node classes that may be inlined.
@@ -1,3 +1,6 @@
1
+ require 'stringio'
2
+ require 'zlib'
3
+
1
4
  module Seafoam
2
5
  module BGV
3
6
  # A parser for BGV files. It's a push-pull streaming interface that you need
@@ -5,8 +8,12 @@ module Seafoam
5
8
  # and some code is duplicated in order to support skipping over parts of the
6
9
  # file that you don't need.
7
10
  class BGVParser
8
- def initialize(source)
9
- @reader = Binary::BinaryReader.for(source)
11
+ def initialize(file)
12
+ data = File.read(file, encoding: Encoding::ASCII_8BIT)
13
+ if data[0..1].bytes == [0x1f, 0x8b]
14
+ data = Zlib.gunzip(data)
15
+ end
16
+ @reader = Binary::IOBinaryReader.new(StringIO.new(data))
10
17
  @group_stack = []
11
18
  @pool = {}
12
19
  @index = 0
@@ -164,6 +171,9 @@ module Seafoam
164
171
  break true
165
172
  when CLOSE_GROUP
166
173
  read_close_group
174
+ when BEGIN_DOCUMENT
175
+ # But what should we do with them?
176
+ skip_props
167
177
  else
168
178
  raise EncodingError, "unknown token 0x#{token.to_s(16)} beginning BGV object"
169
179
  end
@@ -0,0 +1,93 @@
1
+ require 'stringio'
2
+ require 'zlib'
3
+
4
+ module Seafoam
5
+ module CFG
6
+ Code = Struct.new(:arch, :arch_width, :base, :code)
7
+ Comment = Struct.new(:offset, :comment)
8
+ NMethod = Struct.new(:code, :comments)
9
+
10
+ # A parser for CFG files.
11
+ class CFGParser
12
+ def initialize(out, file)
13
+ @out = out
14
+ data = File.read(file, encoding: Encoding::ASCII_8BIT)
15
+ if data[0..1].bytes == [0x1f, 0x8b]
16
+ data = Zlib.gunzip(data)
17
+ end
18
+ @reader = StringIO.new(data)
19
+ @state = :any
20
+ @cfg_name = nil
21
+ end
22
+
23
+ def skip_over_cfg(name)
24
+ loop do
25
+ line = @reader.readline("\n")
26
+ case line
27
+ when "begin_cfg\n"
28
+ @state = :cfg
29
+ @cfg_name = nil
30
+ when / name "(.*)"\n/
31
+ if @state == :cfg
32
+ @cfg_name = Regexp.last_match(1)
33
+ end
34
+ when "end_cfg\n"
35
+ raise unless @state == :cfg
36
+
37
+ @state = :any
38
+ break if @cfg_name == name
39
+ else
40
+ next
41
+ end
42
+ end
43
+ end
44
+
45
+ def read_nmethod
46
+ raise unless @state == :any
47
+
48
+ arch = nil
49
+ arch_width = nil
50
+ code = nil
51
+ comments = []
52
+ raise unless @reader.readline == "begin_nmethod\n"
53
+
54
+ loop do
55
+ line = @reader.readline("\n")
56
+ case line
57
+ when / Platform (.*) (.*) <\|\|@\n/
58
+ arch = Regexp.last_match(1)
59
+ arch_width = Regexp.last_match(2)
60
+ when / HexCode (.*) (.*) <\|\|@\n/
61
+ base = Regexp.last_match(1).to_i(16)
62
+ code = [Regexp.last_match(2)].pack('H*')
63
+ raise if arch.nil? || arch_width.nil?
64
+
65
+ code = Code.new(arch, arch_width, base, code)
66
+ when / Comment (\d*) (.*) <\|\|@\n/
67
+ offset = Regexp.last_match(1).to_i
68
+ comment = Regexp.last_match(2)
69
+ comments.push Comment.new(offset, comment)
70
+ when " <<<HexCodeFile\n"
71
+ next
72
+ when " HexCodeFile>>> <|@\n"
73
+ next
74
+ when "end_nmethod\n"
75
+ break
76
+ when / (.*) <\|\|@\n/
77
+ offset = -1
78
+ comment = Regexp.last_match(1)
79
+ comments.push Comment.new(offset, comment)
80
+ when / (.*)\n/
81
+ offset = -1
82
+ comment = Regexp.last_match(1)
83
+ comments.push Comment.new(offset, comment)
84
+ else
85
+ # In case anything was missed
86
+ raise 'There is currently no case for this line. Please open an issue so it can be addressed.'
87
+ end
88
+ end
89
+ NMethod.new(code, comments)
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,70 @@
1
+ require 'stringio'
2
+
3
+ module Seafoam
4
+ module CFG
5
+ # Disassemble and print comments from cfg files
6
+ class Disassembler
7
+ def initialize(out)
8
+ @out = out
9
+ end
10
+
11
+ def disassemble(nmethod, print_comments)
12
+ require_crabstone
13
+
14
+ comments = nmethod.comments
15
+ comments_n = 0
16
+
17
+ case [nmethod.code.arch, nmethod.code.arch_width]
18
+ when %w[AMD64 64]
19
+ crabstone_arch = [Crabstone::ARCH_X86, Crabstone::MODE_64]
20
+ else
21
+ raise "Unknown architecture #{nmethod.code.arch} and bit width #{nmethod.code.arch_width}"
22
+ end
23
+
24
+ cs = Crabstone::Disassembler.new(*crabstone_arch)
25
+ begin
26
+ cs.disasm(nmethod.code.code, nmethod.code.base).each do |i|
27
+ if print_comments
28
+ # Print comments associated to the instruction.
29
+ last_comment = i.address + i.bytes.length - nmethod.code.base
30
+ while comments_n < comments.length && comments[comments_n].offset < last_comment
31
+ if comments[comments_n].offset == -1
32
+ @out.printf("\t\t\t\t;%<comment>s\n", comment: comments[comments_n].comment)
33
+ else
34
+ @out.printf(
35
+ "\t\t\t\t;Comment %<loc>i:\t%<comment>s\n",
36
+ loc: comments[comments_n].offset,
37
+ comment: comments[comments_n].comment
38
+ )
39
+ end
40
+ comments_n += 1
41
+ end
42
+ end
43
+
44
+ # Print the instruction.
45
+ @out.printf(
46
+ "\t0x%<address>x:\t%<instruction>s\t%<details>s\n",
47
+ address: i.address,
48
+ instruction: i.mnemonic,
49
+ details: i.op_str
50
+ )
51
+ end
52
+ rescue StandardError => e
53
+ raise "Disassembly error: #{e.message}"
54
+ ensure
55
+ cs.close
56
+ end
57
+ end
58
+
59
+ def require_crabstone
60
+ require 'crabstone'
61
+ rescue LoadError => e
62
+ if $DEBUG
63
+ raise e
64
+ else
65
+ raise 'Could not load Capstone - is it installed?'
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -8,12 +8,31 @@ module Seafoam
8
8
  @config = config
9
9
  end
10
10
 
11
- # Run any command.
12
- def run(*args)
11
+ # Run the general seafoam command.
12
+ def seafoam(*args)
13
13
  first, *args = args
14
14
  case first
15
15
  when nil, 'help', '-h', '--help', '-help'
16
- help(*args)
16
+ raise ArgumentError, "unexpected arguments #{args.join(' ')}" unless args.empty?
17
+
18
+ @out.puts 'seafoam file.bgv info'
19
+ @out.puts ' file.bgv list'
20
+ @out.puts ' file.bgv[:graph][:node[-edge]] search term...'
21
+ @out.puts ' file.bgv[:graph][:node[-edge]] edges'
22
+ @out.puts ' file.bgv[:graph][:node[-edge]] props'
23
+ @out.puts ' file.bgv:graph:node source'
24
+ @out.puts ' file.bgv:graph render'
25
+ @out.puts ' --spotlight n,n,n...'
26
+ @out.puts ' --out graph.pdf'
27
+ @out.puts ' graph.svg'
28
+ @out.puts ' graph.png'
29
+ @out.puts ' graph.dot'
30
+ @out.puts ' --show-frame-state'
31
+ @out.puts ' --hide-floating'
32
+ @out.puts ' --no-reduce-edges'
33
+ @out.puts ' --option key value'
34
+ @out.puts ' --help'
35
+ @out.puts ' --version'
17
36
  when 'version', '-v', '-version', '--version'
18
37
  version(*args)
19
38
  else
@@ -32,6 +51,8 @@ module Seafoam
32
51
  edges name, *args
33
52
  when 'props'
34
53
  props name, *args
54
+ when 'source'
55
+ source name, *args
35
56
  when 'render'
36
57
  render name, *args
37
58
  when 'debug'
@@ -42,6 +63,141 @@ module Seafoam
42
63
  end
43
64
  end
44
65
 
66
+ # Run the bgv2isabelle command.
67
+ def bgv2isabelle(*args)
68
+ case args.first
69
+ when nil, 'help', '-h', '--help', '-help'
70
+ args = args.drop(1)
71
+ raise ArgumentError, "unexpected arguments #{args.join(' ')}" unless args.empty?
72
+
73
+ @out.puts 'bgv2isabelle file.bgv...'
74
+ @out.puts ' --help'
75
+ @out.puts ' --version'
76
+ when 'version', '-v', '-version', '--version'
77
+ args = args.drop(1)
78
+ version(*args)
79
+ else
80
+ files = []
81
+
82
+ until args.empty?
83
+ arg = args.shift
84
+ if arg.start_with?('-')
85
+ raise ArgumentError, "unknown option #{arg}"
86
+ else
87
+ files.push arg
88
+ end
89
+ end
90
+
91
+ writer = IsabelleWriter.new(@out)
92
+
93
+ files.each do |file|
94
+ parser = Seafoam::BGV::BGVParser.new(file)
95
+ parser.read_file_header
96
+ parser.skip_document_props
97
+
98
+ loop do
99
+ index, = parser.read_graph_preheader
100
+ break unless index
101
+
102
+ graph_header = parser.read_graph_header
103
+ name = parser.graph_name(graph_header)
104
+ graph = parser.read_graph
105
+
106
+ writer.write index, name, graph
107
+ end
108
+ end
109
+ end
110
+ end
111
+
112
+ def bgv2json(*args)
113
+ case args.first
114
+ when nil, 'help', '-h', '--help', '-help'
115
+ args = args.drop(1)
116
+ raise ArgumentError, "unexpected arguments #{args.join(' ')}" unless args.empty?
117
+
118
+ @out.puts 'bgv2json file.bgv...'
119
+ @out.puts ' --help'
120
+ @out.puts ' --version'
121
+ when 'version', '-v', '-version', '--version'
122
+ args = args.drop(1)
123
+ version(*args)
124
+ else
125
+ files = []
126
+
127
+ until args.empty?
128
+ arg = args.shift
129
+ if arg.start_with?('-')
130
+ raise ArgumentError, "unknown option #{arg}"
131
+ else
132
+ files.push arg
133
+ end
134
+ end
135
+
136
+ writer = JSONWriter.new(@out)
137
+
138
+ files.each do |file|
139
+ parser = Seafoam::BGV::BGVParser.new(file)
140
+ parser.read_file_header
141
+ parser.skip_document_props
142
+
143
+ loop do
144
+ index, = parser.read_graph_preheader
145
+ break unless index
146
+
147
+ graph_header = parser.read_graph_header
148
+ name = parser.graph_name(graph_header)
149
+ graph = parser.read_graph
150
+
151
+ writer.write name, graph
152
+ end
153
+ end
154
+ end
155
+ end
156
+
157
+ def cfg2asm(*args)
158
+ case args.first
159
+ when nil, 'help', '-h', '--help', '-help'
160
+ args = args.drop(1)
161
+ raise ArgumentError, "unexpected arguments #{args.join(' ')}" unless args.empty?
162
+
163
+ @out.puts 'cfg2asm file.bgv...'
164
+ @out.puts ' --no-comments'
165
+ @out.puts ' --help'
166
+ @out.puts ' --version'
167
+ when 'version', '-v', '-version', '--version'
168
+ args = args.drop(1)
169
+ version(*args)
170
+ else
171
+ comments = true
172
+ files = []
173
+
174
+ until args.empty?
175
+ arg = args.shift
176
+ if arg.start_with?('-')
177
+ case arg
178
+ when '--no-comments'
179
+ comments = false
180
+ else
181
+ raise ArgumentError, "unknown option #{arg}"
182
+ end
183
+ else
184
+ files.push arg
185
+ end
186
+ end
187
+
188
+ files.each_with_index do |file, n|
189
+ parser = Seafoam::CFG::CFGParser.new(@out, file)
190
+ parser.skip_over_cfg 'After code installation'
191
+ nmethod = parser.read_nmethod
192
+
193
+ disassembler = Seafoam::CFG::Disassembler.new(@out)
194
+ @out.puts if n.positive?
195
+ @out.puts "[#{file}]"
196
+ disassembler.disassemble(nmethod, comments)
197
+ end
198
+ end
199
+ end
200
+
45
201
  private
46
202
 
47
203
  # seafoam file.bgv info
@@ -51,7 +207,7 @@ module Seafoam
51
207
 
52
208
  raise ArgumentError, 'info does not take arguments' unless args.empty?
53
209
 
54
- parser = BGV::BGVParser.new(File.new(file))
210
+ parser = BGV::BGVParser.new(file)
55
211
  major, minor = parser.read_file_header(version_check: false)
56
212
  @out.puts "BGV #{major}.#{minor}"
57
213
  end
@@ -63,7 +219,7 @@ module Seafoam
63
219
 
64
220
  raise ArgumentError, 'list does not take arguments' unless args.empty?
65
221
 
66
- parser = BGV::BGVParser.new(File.new(file))
222
+ parser = BGV::BGVParser.new(file)
67
223
  parser.read_file_header
68
224
  parser.skip_document_props
69
225
  loop do
@@ -81,7 +237,7 @@ module Seafoam
81
237
  file, graph_index, node_id, = parse_name(name)
82
238
  raise ArgumentError, 'search only works with a file or graph' if node_id
83
239
 
84
- parser = BGV::BGVParser.new(File.new(file))
240
+ parser = BGV::BGVParser.new(file)
85
241
  parser.read_file_header
86
242
  parser.skip_document_props
87
243
  loop do
@@ -211,14 +367,31 @@ module Seafoam
211
367
  end
212
368
  end
213
369
  else
214
- parser = BGV::BGVParser.new(File.new(file))
370
+ parser = BGV::BGVParser.new(file)
215
371
  parser.read_file_header
216
372
  document_props = parser.read_document_props
217
373
  pretty_print document_props || {}
218
374
  end
219
375
  end
220
376
 
221
- # seafoam file.bgv:0 render options...
377
+ # seafoam file.bgv:n:n source
378
+ def source(name, *args)
379
+ file, graph_index, node_id, edge_id = parse_name(name)
380
+ raise ArgumentError, 'source needs a node' unless node_id
381
+ raise ArgumentError, 'source only works with a node' if edge_id
382
+ raise ArgumentError, 'source does not take arguments' unless args.empty?
383
+
384
+ with_graph(file, graph_index) do |parser|
385
+ parser.read_graph_header
386
+ graph = parser.read_graph
387
+ node = graph.nodes[node_id]
388
+ raise ArgumentError, 'node not found' unless node
389
+
390
+ @out.puts Graal::Source.render(node.props['nodeSourcePosition'])
391
+ end
392
+ end
393
+
394
+ # seafoam file.bgv:n render options...
222
395
  def render(name, *args)
223
396
  file, graph_index, *rest = parse_name(name)
224
397
  raise ArgumentError, 'render needs at least a graph' unless graph_index
@@ -299,10 +472,14 @@ module Seafoam
299
472
  writer.write_graph graph
300
473
  end
301
474
  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
475
+ begin
476
+ IO.popen(['dot', "-T#{out_format}", '-o', out_file], 'w') do |stream|
477
+ writer = GraphvizWriter.new(stream)
478
+ hidpi = out_format == :png
479
+ writer.write_graph graph, hidpi
480
+ end
481
+ rescue Errno::ENOENT
482
+ raise 'Could not run Graphviz - is it installed?'
306
483
  end
307
484
  autoopen out_file unless explicit_out_file
308
485
  end
@@ -368,7 +545,7 @@ module Seafoam
368
545
  # Reads a file and yields just the graph requested by the index - skipping
369
546
  # the rest of the file as best as possible.
370
547
  def with_graph(file, graph_index)
371
- parser = BGV::BGVParser.new(File.new(file))
548
+ parser = BGV::BGVParser.new(file)
372
549
  parser.read_file_header
373
550
  parser.skip_document_props
374
551
  graph_found = false
@@ -388,27 +565,6 @@ module Seafoam
388
565
  raise ArgumentError, 'graph not found' unless graph_found
389
566
  end
390
567
 
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
568
  # Prints the version.
413
569
  def version(*args)
414
570
  raise ArgumentError, "unexpected arguments #{args.join(' ')}" unless args.empty?
@@ -418,8 +574,12 @@ module Seafoam
418
574
 
419
575
  # Parse a name like file.bgv:g:n-e to [file.bgv, g, n, e].
420
576
  def parse_name(name)
421
- *pre, file, graph, node = name.split(':')
422
- file = [*pre, file].join(':')
577
+ *pre, post = name.split('.')
578
+
579
+ file_ext, graph, node, *rest = post.split(':')
580
+ raise ArgumentError, "too many parts to .ext:g:n-e in #{name}" unless rest.empty?
581
+
582
+ file = [*pre, file_ext].join('.')
423
583
 
424
584
  if node
425
585
  node, edge, *rest = node.split('-')