diff --git a/.rubocop.yml b/.rubocop.yml index 381d7a27..62e78453 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -90,6 +90,9 @@ Style/CaseLikeIf: Style/ClassVars: Enabled: false +Style/CombinableLoops: + Enabled: false + Style/DocumentDynamicEvalDefinition: Enabled: false diff --git a/lib/syntax_tree.rb b/lib/syntax_tree.rb index ade9ff5e..1af1b476 100644 --- a/lib/syntax_tree.rb +++ b/lib/syntax_tree.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +require "cgi" require "etc" require "fiddle" require "json" @@ -18,6 +19,7 @@ require_relative "syntax_tree/visitor/field_visitor" require_relative "syntax_tree/visitor/json_visitor" require_relative "syntax_tree/visitor/match_visitor" +require_relative "syntax_tree/visitor/mermaid_visitor" require_relative "syntax_tree/visitor/mutation_visitor" require_relative "syntax_tree/visitor/pretty_print_visitor" require_relative "syntax_tree/visitor/environment" diff --git a/lib/syntax_tree/node.rb b/lib/syntax_tree/node.rb index 1a814aaf..b1ecfdc7 100644 --- a/lib/syntax_tree/node.rb +++ b/lib/syntax_tree/node.rb @@ -127,17 +127,19 @@ def format(q) end def pretty_print(q) - visitor = Visitor::PrettyPrintVisitor.new(q) - visitor.visit(self) + accept(Visitor::PrettyPrintVisitor.new(q)) end def to_json(*opts) - visitor = Visitor::JSONVisitor.new - visitor.visit(self).to_json(*opts) + accept(Visitor::JSONVisitor.new).to_json(*opts) + end + + def to_mermaid + accept(Visitor::MermaidVisitor.new) end def construct_keys - PrettierPrint.format(+"") { |q| Visitor::MatchVisitor.new(q).visit(self) } + PrettierPrint.format(+"") { |q| accept(Visitor::MatchVisitor.new(q)) } end end diff --git a/lib/syntax_tree/visitor/mermaid_visitor.rb b/lib/syntax_tree/visitor/mermaid_visitor.rb new file mode 100644 index 00000000..2b06049a --- /dev/null +++ b/lib/syntax_tree/visitor/mermaid_visitor.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +module SyntaxTree + class Visitor + # This visitor transforms the AST into a mermaid flow chart. + class MermaidVisitor < FieldVisitor + attr_reader :output, :target + + def initialize + @output = StringIO.new + @output.puts("flowchart TD") + + @target = nil + end + + def visit_program(node) + super + output.string + end + + private + + def comments(node) + # Ignore + end + + def field(name, value) + case value + when Node + node_id = visit(value) + output.puts(" #{target} -- \"#{name}\" --> #{node_id}") + when String + node_id = "#{target}_#{name}" + output.puts(" #{node_id}([#{CGI.escapeHTML(value.inspect)}])") + output.puts(" #{target} -- \"#{name}\" --> #{node_id}") + when nil + # skip + else + node_id = "#{target}_#{name}" + output.puts(" #{node_id}([\"#{CGI.escapeHTML(value.inspect)}\"])") + output.puts(" #{target} -- \"#{name}\" --> #{node_id}") + end + end + + def list(name, values) + values.each_with_index do |value, index| + field("#{name}[#{index}]", value) + end + end + + def node(node, type) + previous_target = target + + begin + @target = "node_#{node.object_id}" + + yield + + output.puts(" #{@target}[\"#{type}\"]") + @target + ensure + @target = previous_target + end + end + + def pairs(name, values) + values.each_with_index do |(key, value), index| + node_id = "#{target}_#{name}_#{index}" + output.puts(" #{node_id}((\" \"))") + output.puts(" #{target} -- \"#{name}[#{index}]\" --> #{node_id}") + output.puts(" #{node_id} -- \"[0]\" --> #{visit(key)}") + output.puts(" #{node_id} -- \"[1]\" --> #{visit(value)}") if value + end + end + + def text(name, value) + field(name, value) + end + end + end +end diff --git a/lib/syntax_tree/yarv/control_flow_graph.rb b/lib/syntax_tree/yarv/control_flow_graph.rb index dc900e50..328ffc4c 100644 --- a/lib/syntax_tree/yarv/control_flow_graph.rb +++ b/lib/syntax_tree/yarv/control_flow_graph.rb @@ -55,6 +55,41 @@ def disasm fmt.string end + def to_mermaid + output = StringIO.new + output.puts("flowchart TD") + + fmt = Disassembler::Mermaid.new + blocks.each do |block| + output.puts(" subgraph #{block.id}") + previous = nil + + block.each_with_length do |insn, length| + node_id = "node_#{length}" + label = "%04d %s" % [length, insn.disasm(fmt)] + + output.puts(" #{node_id}(\"#{CGI.escapeHTML(label)}\")") + output.puts(" #{previous} --> #{node_id}") if previous + + previous = node_id + end + + output.puts(" end") + end + + blocks.each do |block| + block.outgoing_blocks.each do |outgoing| + offset = + block.block_start + block.insns.sum(&:length) - + block.insns.last.length + + output.puts(" node_#{offset} --> node_#{outgoing.block_start}") + end + end + + output.string + end + # This method is used to verify that the control flow graph is well # formed. It does this by checking that each basic block is itself well # formed. diff --git a/lib/syntax_tree/yarv/data_flow_graph.rb b/lib/syntax_tree/yarv/data_flow_graph.rb index f98eedda..7423d022 100644 --- a/lib/syntax_tree/yarv/data_flow_graph.rb +++ b/lib/syntax_tree/yarv/data_flow_graph.rb @@ -80,6 +80,67 @@ def disasm fmt.string end + def to_mermaid + output = StringIO.new + output.puts("flowchart TD") + + fmt = Disassembler::Mermaid.new + links = [] + + cfg.blocks.each do |block| + block_flow = block_flows.fetch(block.id) + graph_name = + if block_flow.in.any? + "#{block.id} #{block_flows[block.id].in.join(", ")}" + else + block.id + end + + output.puts(" subgraph \"#{CGI.escapeHTML(graph_name)}\"") + previous = nil + + block.each_with_length do |insn, length| + node_id = "node_#{length}" + label = "%04d %s" % [length, insn.disasm(fmt)] + + output.puts(" #{node_id}(\"#{CGI.escapeHTML(label)}\")") + + if previous + output.puts(" #{previous} --> #{node_id}") + links << "red" + end + + insn_flows[length].in.each do |input| + if input.is_a?(Integer) + output.puts(" node_#{input} --> #{node_id}") + links << "green" + end + end + + previous = node_id + end + + output.puts(" end") + end + + cfg.blocks.each do |block| + block.outgoing_blocks.each do |outgoing| + offset = + block.block_start + block.insns.sum(&:length) - + block.insns.last.length + + output.puts(" node_#{offset} --> node_#{outgoing.block_start}") + links << "red" + end + end + + links.each_with_index do |color, index| + output.puts(" linkStyle #{index} stroke:#{color}") + end + + output.string + end + # Verify that we constructed the data flow graph correctly. def verify # Check that the first block has no arguments. diff --git a/lib/syntax_tree/yarv/disassembler.rb b/lib/syntax_tree/yarv/disassembler.rb index a758bce3..f60af0fd 100644 --- a/lib/syntax_tree/yarv/disassembler.rb +++ b/lib/syntax_tree/yarv/disassembler.rb @@ -3,6 +3,41 @@ module SyntaxTree module YARV class Disassembler + # This class is another object that handles disassembling a YARV + # instruction sequence but it does so in order to provide a label for a + # mermaid diagram. + class Mermaid + def calldata(value) + value.inspect + end + + def enqueue(iseq) + end + + def event(name) + end + + def inline_storage(cache) + "" + end + + def instruction(name, operands = []) + operands.empty? ? name : "#{name} #{operands.join(", ")}" + end + + def label(value) + "%04d" % value.name["label_".length..] + end + + def local(index, **) + index.inspect + end + + def object(value) + value.inspect + end + end + attr_reader :output, :queue attr_reader :current_prefix