From 4ec195bef0f61cbd098119eab56bc16190dd925b Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Mon, 6 Feb 2023 13:55:59 -0500 Subject: [PATCH 1/3] Mermaid visitor --- lib/syntax_tree.rb | 2 + lib/syntax_tree/node.rb | 12 ++-- lib/syntax_tree/visitor/mermaid_visitor.rb | 81 ++++++++++++++++++++++ 3 files changed, 90 insertions(+), 5 deletions(-) create mode 100644 lib/syntax_tree/visitor/mermaid_visitor.rb 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..8ffbcd2d 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 construct_keys - PrettierPrint.format(+"") { |q| Visitor::MatchVisitor.new(q).visit(self) } + PrettierPrint.format(+"") { |q| accept(Visitor::MatchVisitor.new(q)) } + end + + def mermaid + accept(Visitor::MermaidVisitor.new) 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 From e7c5adf1de9fcac198fdbbdc1350515c3bf02210 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Mon, 6 Feb 2023 14:12:36 -0500 Subject: [PATCH 2/3] Control flow graphs to mermaid --- .rubocop.yml | 3 ++ lib/syntax_tree/node.rb | 8 ++--- lib/syntax_tree/yarv/control_flow_graph.rb | 34 +++++++++++++++++++++ lib/syntax_tree/yarv/disassembler.rb | 35 ++++++++++++++++++++++ 4 files changed, 76 insertions(+), 4 deletions(-) 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/node.rb b/lib/syntax_tree/node.rb index 8ffbcd2d..b1ecfdc7 100644 --- a/lib/syntax_tree/node.rb +++ b/lib/syntax_tree/node.rb @@ -134,12 +134,12 @@ def to_json(*opts) accept(Visitor::JSONVisitor.new).to_json(*opts) end - def construct_keys - PrettierPrint.format(+"") { |q| accept(Visitor::MatchVisitor.new(q)) } + def to_mermaid + accept(Visitor::MermaidVisitor.new) end - def mermaid - accept(Visitor::MermaidVisitor.new) + def construct_keys + PrettierPrint.format(+"") { |q| accept(Visitor::MatchVisitor.new(q)) } end end diff --git a/lib/syntax_tree/yarv/control_flow_graph.rb b/lib/syntax_tree/yarv/control_flow_graph.rb index dc900e50..a9f3e093 100644 --- a/lib/syntax_tree/yarv/control_flow_graph.rb +++ b/lib/syntax_tree/yarv/control_flow_graph.rb @@ -55,6 +55,40 @@ 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/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 From e642348dc2da3e2a8299ebc9e56b0fe6e965446f Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Mon, 6 Feb 2023 14:32:44 -0500 Subject: [PATCH 3/3] DFG to mermaid --- lib/syntax_tree/yarv/control_flow_graph.rb | 1 + lib/syntax_tree/yarv/data_flow_graph.rb | 61 ++++++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/lib/syntax_tree/yarv/control_flow_graph.rb b/lib/syntax_tree/yarv/control_flow_graph.rb index a9f3e093..328ffc4c 100644 --- a/lib/syntax_tree/yarv/control_flow_graph.rb +++ b/lib/syntax_tree/yarv/control_flow_graph.rb @@ -82,6 +82,7 @@ def to_mermaid offset = block.block_start + block.insns.sum(&:length) - block.insns.last.length + output.puts(" node_#{offset} --> node_#{outgoing.block_start}") end end 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.