Skip to content

Commit

Permalink
Merge pull request ruby#523 from ydah/diagram
Browse files Browse the repository at this point in the history
Add diagram generation feature and integrate Railroad Diagrams
  • Loading branch information
ydah authored Feb 3, 2025
2 parents 88097af + 9113e0b commit e156993
Show file tree
Hide file tree
Showing 16 changed files with 919 additions and 2 deletions.
4 changes: 3 additions & 1 deletion .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ jobs:
- uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}
bundler-cache: true
# NOTE: If this cache is present, the following will fail at Ruby 2.5:
# see: https://github.com/ruby/lrama/actions/runs/13088401502/job/36522284488
# bundler-cache: true
- run: flex --help
- run: bundle install
- run: bundle exec rspec
Expand Down
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ gem "rspec"
gem "simplecov", require: false
gem "stackprof", platforms: [:ruby] # stackprof doesn't support Windows
gem "memory_profiler"
gem "railroad_diagrams", "0.2.1"

# Recent steep requires Ruby >= 3.0.0.
# Then skip install on some CI jobs.
Expand Down
14 changes: 14 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
# NEWS for Lrama

## Lrama 0.7.1 (2025-xx-xx)

### Syntax Diagrams

Lrama provides an API for generating HTML syntax diagrams. These visual diagrams are highly useful as grammar development tools and can also serve as a form of automatic self-documentation.

![Syntax Diagrams](https://github.com/user-attachments/assets/5d9bca77-93fd-4416-bc24-9a0f70693a22)

If you use syntax diagrams, you add `--diagram` option.

```console
$ exe/lrama --diagram sample.y
```

## Lrama 0.7.0 (2025-01-21)

### [EXPERIMENTAL] Support the generation of the IELR(1) parser described in this paper
Expand Down
1 change: 1 addition & 0 deletions lib/lrama.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
require_relative "lrama/context"
require_relative "lrama/counterexamples"
require_relative "lrama/diagnostics"
require_relative "lrama/diagram"
require_relative "lrama/digraph"
require_relative "lrama/grammar"
require_relative "lrama/grammar_validator"
Expand Down
6 changes: 6 additions & 0 deletions lib/lrama/command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ def run(argv)
reporter = Lrama::TraceReporter.new(grammar)
reporter.report(**options.trace_opts)

if options.diagram
File.open(options.diagram_file, "w+") do |f|
Lrama::Diagram.render(out: f, grammar: grammar)
end
end

File.open(options.outfile, "w+") do |f|
Lrama::Output.new(
out: f,
Expand Down
85 changes: 85 additions & 0 deletions lib/lrama/diagram.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# frozen_string_literal: true

require "erb"

module Lrama
class Diagram
class << self
def render(out:, grammar:, template_name: 'diagram/diagram.html')
return unless require_railroad_diagrams
new(out: out, grammar: grammar, template_name: template_name).render
end

def require_railroad_diagrams
require "railroad_diagrams"
true
rescue LoadError
warn "railroad_diagrams is not installed. Please run `bundle install`."
false
end
end

def initialize(out:, grammar:, template_name: 'diagram/diagram.html')
@grammar = grammar
@out = out
@template_name = template_name
end

if ERB.instance_method(:initialize).parameters.last.first == :key
def self.erb(input)
ERB.new(input, trim_mode: nil)
end
else
def self.erb(input)
ERB.new(input, nil, nil)
end
end

def render
RailroadDiagrams::TextDiagram.set_formatting(RailroadDiagrams::TextDiagram::PARTS_UNICODE)
@out << render_template(template_file)
end

def default_style
RailroadDiagrams::Style::default_style
end

def diagrams
result = +''
@grammar.unique_rule_s_values.each do |s_value|
diagrams =
@grammar.select_rules_by_s_value(s_value).map { |r| r.to_diagrams }
add_diagram(
s_value,
RailroadDiagrams::Diagram.new(
RailroadDiagrams::Choice.new(0, *diagrams),
),
result
)
end
result
end

private

def render_template(file)
erb = self.class.erb(File.read(file))
erb.filename = file
erb.result_with_hash(output: self)
end

def template_dir
File.expand_path('../../template', __dir__)
end

def template_file
File.join(template_dir, @template_name)
end

def add_diagram(name, diagram, result)
result << "\n<h2>#{RailroadDiagrams.escape_html(name)}</h2>"
diagram.write_svg(result.method(:<<))
result << "\n"
end
end
end
8 changes: 8 additions & 0 deletions lib/lrama/grammar.rb
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,14 @@ def find_rules_by_symbol(sym)
@sym_to_rules[sym.number]
end

def select_rules_by_s_value(s_value)
@rules.select {|rule| rule.lhs.id.s_value == s_value }
end

def unique_rule_s_values
@rules.map {|rule| rule.lhs.id.s_value }.uniq
end

def ielr_defined?
@define.key?('lr.type') && @define['lr.type'] == 'ielr'
end
Expand Down
20 changes: 20 additions & 0 deletions lib/lrama/grammar/rule.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ def display_name_without_action
"#{l} -> #{r}"
end

def to_diagrams
if rhs.empty?
RailroadDiagrams::Skip.new
else
RailroadDiagrams::Sequence.new(*rhs_to_diagram)
end
end

# Used by #user_actions
def as_comment
l = lhs.id.s_value
Expand Down Expand Up @@ -70,6 +78,18 @@ def contains_at_reference?

token_code.references.any? {|r| r.type == :at }
end

private

def rhs_to_diagram
rhs.map do |r|
if r.term
RailroadDiagrams::Terminal.new(r.id.s_value)
else
RailroadDiagrams::NonTerminal.new(r.id.s_value)
end
end
end
end
end
end
4 changes: 4 additions & 0 deletions lib/lrama/option_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ def parse_by_option_parser(argv)
o.on_tail ' time display generation time'
o.on_tail ' all include all the above traces'
o.on_tail ' none disable all traces'
o.on('--diagram=[FILE]', 'generate a diagram of the rules') do |v|
@options.diagram = true
@options.diagram_file = v if v
end
o.on('-v', '--verbose', "same as '--report=state'") {|_v| @report << 'states' }
o.separator ''
o.separator 'Diagnostics:'
Expand Down
5 changes: 4 additions & 1 deletion lib/lrama/options.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ class Options
:report_file, :outfile,
:error_recovery, :grammar_file,
:trace_opts, :report_opts,
:diagnostic, :y, :debug, :define
:diagnostic, :y, :debug, :define,
:diagram, :diagram_file

def initialize
@skeleton = "bison/yacc.c"
Expand All @@ -23,6 +24,8 @@ def initialize
@diagnostic = false
@y = STDIN
@debug = false
@diagram = false
@diagram_file = "diagram.html"
end
end
end
Loading

0 comments on commit e156993

Please sign in to comment.