From 0dea8197d782f1aa445fe0441a45bcfe90eccd49 Mon Sep 17 00:00:00 2001 From: ydah Date: Sat, 1 Feb 2025 19:45:44 +0900 Subject: [PATCH 1/7] Add diagram generation feature and integrate Railroad 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) This feature was inspired by the functionality of [Chevrotain](https://chevrotain.io/docs/), a parser construction toolkit for JavaScript. The main motivation for adding this feature was to make the structure of `parse.y`, and thus the structure of Ruby's grammar, easier to understand. I thought that generating syntax diagrams in a more human-readable format would be beneficial. These diagrams are highly useful as grammar development tools and can also serve as automatic self-documentation, which I believe is a significant advancement. Chevrotain used [tabatkins/railroad-diagrams](https://github.com/tabatkins/railroad-diagrams), but this library only supported JavaScript and Python. To address this, I created a Ruby library and integrated it into Lrama: [https://github.com/ydah/railroad_diagrams](https://github.com/ydah/railroad_diagrams) This makes collaboration with Lrama easier and allows improvements to the library based on Lrama's needs. For these reasons, I decided to release it as a Ruby gem. --- Gemfile | 4 + NEWS.md | 8 + lib/lrama.rb | 1 + lib/lrama/command.rb | 6 + lib/lrama/diagram.rb | 71 +++ lib/lrama/grammar.rb | 8 + lib/lrama/grammar/rule.rb | 20 + lib/lrama/option_parser.rb | 4 + lib/lrama/options.rb | 5 +- sample/diagram.html | 660 ++++++++++++++++++++ sig/lrama/grammar/rule.rbs | 2 + sig/railroad_diagrams/railroad_diagrams.rbs | 14 + spec/lrama/diagram_spec.rb | 35 ++ spec/lrama/option_parser_spec.rb | 1 + template/diagram/diagram.html | 32 + 15 files changed, 870 insertions(+), 1 deletion(-) create mode 100644 lib/lrama/diagram.rb create mode 100644 sample/diagram.html create mode 100644 sig/railroad_diagrams/railroad_diagrams.rbs create mode 100644 spec/lrama/diagram_spec.rb create mode 100644 template/diagram/diagram.html diff --git a/Gemfile b/Gemfile index 2bf36245..8b73c600 100644 --- a/Gemfile +++ b/Gemfile @@ -13,6 +13,10 @@ gem "simplecov", require: false gem "stackprof", platforms: [:ruby] # stackprof doesn't support Windows gem "memory_profiler" +if RUBY_VERSION >= "3.1.0" + gem "railroad_diagrams" +end + # Recent steep requires Ruby >= 3.0.0. # Then skip install on some CI jobs. if !ENV['GITHUB_ACTION'] || ENV['INSTALL_STEEP'] == 'true' diff --git a/NEWS.md b/NEWS.md index 8ebc5d63..e60df4ba 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,13 @@ # 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) + ## Lrama 0.7.0 (2025-01-21) ### [EXPERIMENTAL] Support the generation of the IELR(1) parser described in this paper diff --git a/lib/lrama.rb b/lib/lrama.rb index fe2e0580..de3b907e 100644 --- a/lib/lrama.rb +++ b/lib/lrama.rb @@ -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" diff --git a/lib/lrama/command.rb b/lib/lrama/command.rb index 3ff39d57..9a02e1be 100644 --- a/lib/lrama/command.rb +++ b/lib/lrama/command.rb @@ -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.new(out: f, grammar: grammar).render + end + end + File.open(options.outfile, "w+") do |f| Lrama::Output.new( out: f, diff --git a/lib/lrama/diagram.rb b/lib/lrama/diagram.rb new file mode 100644 index 00000000..52aee680 --- /dev/null +++ b/lib/lrama/diagram.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +require "erb" +require "railroad_diagrams" + +module Lrama + class Diagram + def initialize(out:, grammar:, template_name: 'diagram/diagram.html') + @grammar = grammar + @out = out + @template_name = template_name + RailroadDiagrams::TextDiagram.set_formatting(RailroadDiagrams::TextDiagram::PARTS_UNICODE) + 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 + @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

#{RailroadDiagrams.escape_html(name)}

" + diagram.write_svg(result.method(:<<)) + result << "\n" + end + end +end diff --git a/lib/lrama/grammar.rb b/lib/lrama/grammar.rb index 214ca1a3..8c560e7a 100644 --- a/lib/lrama/grammar.rb +++ b/lib/lrama/grammar.rb @@ -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 diff --git a/lib/lrama/grammar/rule.rb b/lib/lrama/grammar/rule.rb index 445752ae..20ff90cd 100644 --- a/lib/lrama/grammar/rule.rb +++ b/lib/lrama/grammar/rule.rb @@ -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 @@ -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 diff --git a/lib/lrama/option_parser.rb b/lib/lrama/option_parser.rb index 23988a5f..e2b2e60e 100644 --- a/lib/lrama/option_parser.rb +++ b/lib/lrama/option_parser.rb @@ -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:' diff --git a/lib/lrama/options.rb b/lib/lrama/options.rb index 08f75a77..a8b01672 100644 --- a/lib/lrama/options.rb +++ b/lib/lrama/options.rb @@ -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" @@ -23,6 +24,8 @@ def initialize @diagnostic = false @y = STDIN @debug = false + @diagram = false + @diagram_file = "diagram.html" end end end diff --git a/sample/diagram.html b/sample/diagram.html new file mode 100644 index 00000000..9e86cf6d --- /dev/null +++ b/sample/diagram.html @@ -0,0 +1,660 @@ + + + + Lrama syntax diagrams + + + + + + + +

$accept

programEND_OF_INPUT + +

$@1

+ +

option_terms

terms + +

compstmt_top_stmts

top_stmtsoption_terms + +

program

$@1compstmt_top_stmts + +

top_stmts

nonetop_stmttop_stmtstermstop_stmt + +

top_stmt

stmtkeyword_BEGINbegin_block + +

block_open

'{' + +

begin_block

block_opencompstmt_top_stmts'}' + +

compstmt_stmts

stmtsoption_terms + +

$@2

+ +

$@3

+ +

bodystmt

compstmt_stmtslex_ctxtopt_rescuek_else$@2compstmt_stmts$@3opt_ensurecompstmt_stmtslex_ctxtopt_rescue$@4opt_ensure + +

$@4

+ +

stmts

nonestmt_or_beginstmtstermsstmt_or_begin + +

stmt_or_begin

stmtkeyword_BEGIN$@5begin_block + +

$@5

+ +

allow_exits

+ +

k_END

keyword_ENDlex_ctxt + +

$@6

+ +

stmt

keyword_aliasfitem$@6fitemkeyword_aliastGVARtGVARkeyword_aliastGVARtBACK_REFkeyword_aliastGVARtNTH_REFkeyword_undefundef_liststmtmodifier_ifexpr_valuestmtmodifier_unlessexpr_valuestmtmodifier_whileexpr_valuestmtmodifier_untilexpr_valuestmtmodifier_rescueafter_rescuestmtk_ENDallow_exits'{'compstmt_stmts'}'command_asgnmlhs'='lex_ctxtcommand_call_valueasgn_lhs_mrhsmlhs'='lex_ctxtmrhs_argmodifier_rescueafter_rescuestmtmlhs'='lex_ctxtmrhs_argexprYYerror + +

asgn_lhs_mrhs

lhs'='lex_ctxtmrhs + +

asgn_lhs_command_rhs

lhs'='lex_ctxtcommand_rhs + +

command_asgn

asgn_lhs_command_rhsop_asgn_command_rhsdef_endless_method_endless_command + +

op_asgn_command_rhs

var_lhstOP_ASGNlex_ctxtcommand_rhsprimary_value'['opt_call_argsrbrackettOP_ASGNlex_ctxtcommand_rhsprimary_valuecall_optIDENTIFIERtOP_ASGNlex_ctxtcommand_rhsprimary_valuecall_optCONSTANTtOP_ASGNlex_ctxtcommand_rhsprimary_valuetCOLON2tIDENTIFIERtOP_ASGNlex_ctxtcommand_rhsprimary_valuetCOLON2tCONSTANTtOP_ASGNlex_ctxtcommand_rhsbackreftOP_ASGNlex_ctxtcommand_rhs + +

def_endless_method_endless_command

defn_headf_opt_paren_args'='endless_commanddefs_headf_opt_paren_args'='endless_command + +

endless_command

commandendless_commandmodifier_rescueafter_rescueargkeyword_notoption_'\n'endless_command + +

option_'\n'

'\n' + +

command_rhs

command_call_valuecommand_call_valuemodifier_rescueafter_rescuestmtcommand_asgn + +

expr

command_callexprkeyword_andexprexprkeyword_orexprkeyword_notoption_'\n'expr'!'command_callargtASSOC$@7p_in_kwargp_pvtblp_pktblp_top_expr_bodyargkeyword_in$@8p_in_kwargp_pvtblp_pktblp_top_expr_bodyarg + +

$@7

+ +

$@8

+ +

def_name

fname + +

defn_head

k_defdef_name + +

$@9

+ +

defs_head

k_defsingletondot_or_colon$@9def_name + +

value_expr_expr

expr + +

expr_value

value_expr_exprYYerror + +

$@10

+ +

$@11

+ +

expr_value_do

$@10expr_valuedo$@11 + +

command_call

commandblock_command + +

value_expr_command_call

command_call + +

command_call_value

value_expr_command_call + +

block_command

block_callblock_callcall_op2operation2command_args + +

cmd_brace_block

tLBRACE_ARGbrace_body'}' + +

fcall

operation + +

command

fcallcommand_argsfcallcommand_argscmd_brace_blockprimary_valuecall_opoperation2command_argsprimary_valuecall_opoperation2command_argscmd_brace_blockprimary_valuetCOLON2operation2command_argsprimary_valuetCOLON2operation2command_argscmd_brace_blockprimary_valuetCOLON2tCONSTANT'{'brace_body'}'keyword_supercommand_argsk_yieldcommand_argsk_returncall_argskeyword_breakcall_argskeyword_nextcall_args + +

mlhs

mlhs_basictLPARENmlhs_innerrparen + +

mlhs_inner

mlhs_basictLPARENmlhs_innerrparen + +

mlhs_basic

mlhs_headmlhs_headmlhs_itemmlhs_headtSTARmlhs_nodemlhs_headtSTARmlhs_node','mlhs_postmlhs_headtSTARmlhs_headtSTAR','mlhs_posttSTARmlhs_nodetSTARmlhs_node','mlhs_posttSTARtSTAR','mlhs_post + +

mlhs_item

mlhs_nodetLPARENmlhs_innerrparen + +

mlhs_head

mlhs_item','mlhs_headmlhs_item',' + +

mlhs_post

mlhs_itemmlhs_post','mlhs_item + +

mlhs_node

user_variablekeyword_variableprimary_value'['opt_call_argsrbracketprimary_valuecall_optIDENTIFIERprimary_valuecall_optCONSTANTprimary_valuetCOLON2tIDENTIFIERprimary_valuetCOLON2tCONSTANTtCOLON3tCONSTANTbackref + +

lhs

user_variablekeyword_variableprimary_value'['opt_call_argsrbracketprimary_valuecall_optIDENTIFIERprimary_valuetCOLON2tIDENTIFIERprimary_valuecall_optCONSTANTprimary_valuetCOLON2tCONSTANTtCOLON3tCONSTANTbackref + +

cname

tIDENTIFIERtCONSTANT + +

cpath

tCOLON3cnamecnameprimary_valuetCOLON2cname + +

fname

tIDENTIFIERtCONSTANTtFIDopreswords + +

fitem

fnamesymbol + +

undef_list

fitemundef_list','$@12fitem + +

$@12

+ +

op

'|''^''&'tCMPtEQtEQQtMATCHtNMATCH'>'tGEQ'<'tLEQtNEQtLSHFTtRSHFT'+''-''*'tSTAR'/''%'tPOWtDSTAR'!''~'tUPLUStUMINUStAREFtASET'`' + +

reswords

keyword__LINE__keyword__FILE__keyword__ENCODING__keyword_BEGINkeyword_ENDkeyword_aliaskeyword_andkeyword_beginkeyword_breakkeyword_casekeyword_classkeyword_defkeyword_definedkeyword_dokeyword_elsekeyword_elsifkeyword_endkeyword_ensurekeyword_falsekeyword_forkeyword_inkeyword_modulekeyword_nextkeyword_nilkeyword_notkeyword_orkeyword_redokeyword_rescuekeyword_retrykeyword_returnkeyword_selfkeyword_superkeyword_thenkeyword_truekeyword_undefkeyword_whenkeyword_yieldkeyword_ifkeyword_unlesskeyword_whilekeyword_until + +

asgn_lhs_arg_rhs

lhs'='lex_ctxtarg_rhs + +

arg

asgn_lhs_arg_rhsop_asgn_arg_rhstCOLON3tCONSTANTtOP_ASGNlex_ctxtarg_rhsargtDOT2argargtDOT3argargtDOT2argtDOT3tBDOT2argtBDOT3argarg'+'argarg'-'argarg'*'argarg'/'argarg'%'argargtPOWargtUMINUS_NUMsimple_numerictPOWargtUPLUSargtUMINUSargarg'|'argarg'^'argarg'&'argargtCMPargrel_exprargtEQargargtEQQargargtNEQargargtMATCHargargtNMATCHarg'!'arg'~'argargtLSHFTargargtRSHFTargargtANDOPargargtOROPargkeyword_definedoption_'\n'begin_definedargarg'?'argoption_'\n'':'argdef_endless_method_endless_argprimary + +

op_asgn_arg_rhs

var_lhstOP_ASGNlex_ctxtarg_rhsprimary_value'['opt_call_argsrbrackettOP_ASGNlex_ctxtarg_rhsprimary_valuecall_optIDENTIFIERtOP_ASGNlex_ctxtarg_rhsprimary_valuecall_optCONSTANTtOP_ASGNlex_ctxtarg_rhsprimary_valuetCOLON2tIDENTIFIERtOP_ASGNlex_ctxtarg_rhsprimary_valuetCOLON2tCONSTANTtOP_ASGNlex_ctxtarg_rhsbackreftOP_ASGNlex_ctxtarg_rhs + +

def_endless_method_endless_arg

defn_headf_opt_paren_args'='endless_argdefs_headf_opt_paren_args'='endless_arg + +

endless_arg

argendless_argmodifier_rescueafter_rescueargkeyword_notoption_'\n'endless_arg + +

relop

'>''<'tGEQtLEQ + +

rel_expr

argrelopargrel_exprreloparg + +

lex_ctxt

none + +

begin_defined

lex_ctxt + +

after_rescue

lex_ctxt + +

value_expr_arg

arg + +

arg_value

value_expr_arg + +

aref_args

noneargstrailerargs','assocstrailerassocstrailer + +

arg_rhs

argargmodifier_rescueafter_rescuearg + +

paren_args

'('opt_call_argsrparen'('args','args_forwardrparen'('args_forwardrparen + +

opt_paren_args

noneparen_args + +

opt_call_args

nonecall_argsargs','args','assocs','assocs',' + +

value_expr_command

command + +

call_args

value_expr_commandargsopt_block_argassocsopt_block_argargs','assocsopt_block_argblock_arg + +

$@13

+ +

command_args

$@13call_args + +

block_arg

tAMPERarg_valuetAMPER + +

opt_block_arg

','block_argnone + +

args

arg_valuearg_splatargs','arg_valueargs','arg_splat + +

arg_splat

tSTARarg_valuetSTAR + +

mrhs_arg

mrhsarg_value + +

mrhs

args','arg_valueargs','tSTARarg_valuetSTARarg_value + +

primary

literalstringsxstringregexpwordsqwordssymbolsqsymbolsvar_refbackreftFIDk_begin$@14bodystmtk_endtLPAREN_ARGcompstmt_stmts$@15')'tLPARENcompstmt_stmts')'primary_valuetCOLON2tCONSTANTtCOLON3tCONSTANTtLBRACKaref_args']'tLBRACEassoc_list'}'k_returnk_yield'('call_argsrparenk_yield'('rparenk_yieldkeyword_definedoption_'\n''('begin_definedexprrparenkeyword_not'('exprrparenkeyword_not'('rparenfcallbrace_blockmethod_callmethod_callbrace_blocklambdak_ifexpr_valuethencompstmt_stmtsif_tailk_endk_unlessexpr_valuethencompstmt_stmtsopt_elsek_endk_whileexpr_value_docompstmt_stmtsk_endk_untilexpr_value_docompstmt_stmtsk_endk_caseexpr_valueoption_terms@16case_bodyk_endk_caseoption_terms@17case_bodyk_endk_caseexpr_valueoption_termsp_case_bodyk_endk_forfor_varkeyword_in$@18expr_valuedo$@19compstmt_stmtsk_endk_classcpathsuperclass$@20bodystmtk_endk_classtLSHFTexpr_value$@21termbodystmtk_endk_modulecpath$@22bodystmtk_enddefn_headf_arglist$@23bodystmtk_enddefs_headf_arglist$@24bodystmtk_endkeyword_breakkeyword_nextkeyword_redokeyword_retry + +

$@14

+ +

$@15

+ +

@16

+ +

@17

+ +

$@18

+ +

$@19

+ +

$@20

+ +

$@21

+ +

$@22

+ +

$@23

+ +

$@24

+ +

value_expr_primary

primary + +

primary_value

value_expr_primary + +

k_begin

keyword_begin + +

k_if

keyword_if + +

k_unless

keyword_unless + +

k_while

keyword_whileallow_exits + +

k_until

keyword_untilallow_exits + +

k_case

keyword_case + +

k_for

keyword_forallow_exits + +

k_class

keyword_class + +

k_module

keyword_module + +

k_def

keyword_def + +

k_do

keyword_do + +

k_do_block

keyword_do_block + +

k_rescue

keyword_rescue + +

k_ensure

keyword_ensure + +

k_when

keyword_when + +

k_else

keyword_else + +

k_elsif

keyword_elsif + +

k_end

keyword_endtDUMNY_END + +

k_return

keyword_return + +

k_yield

keyword_yield + +

then

termkeyword_thentermkeyword_then + +

do

termkeyword_do_cond + +

if_tail

opt_elsek_elsifexpr_valuethencompstmt_stmtsif_tail + +

opt_else

nonek_elsecompstmt_stmts + +

for_var

lhsmlhs + +

f_marg

f_norm_argtLPARENf_margsrparen + +

f_marg_list

f_margf_marg_list','f_marg + +

f_margs

f_marg_listf_marg_list','f_rest_margf_marg_list','f_rest_marg','f_marg_listf_rest_margf_rest_marg','f_marg_list + +

f_rest_marg

tSTARf_norm_argtSTAR + +

f_any_kwrest

f_kwrestf_no_kwarg + +

$@25

+ +

f_eq

$@25'=' + +

f_kw_primary_value

f_labelprimary_valuef_label + +

f_kwarg_primary_value

f_kw_primary_valuef_kwarg_primary_value','f_kw_primary_value + +

block_args_tail

f_kwarg_primary_value','f_kwrestopt_f_block_argf_kwarg_primary_valueopt_f_block_argf_any_kwrestopt_f_block_argf_block_arg + +

excessed_comma

',' + +

f_opt_primary_value

f_arg_asgnf_eqprimary_value + +

f_optarg_primary_value

f_opt_primary_valuef_optarg_primary_value','f_opt_primary_value + +

opt_args_tail_block_args_tail

','block_args_tail + +

block_param

f_arg','f_optarg_primary_value','f_rest_argopt_args_tail_block_args_tailf_arg','f_optarg_primary_value','f_rest_arg','f_argopt_args_tail_block_args_tailf_arg','f_optarg_primary_valueopt_args_tail_block_args_tailf_arg','f_optarg_primary_value','f_argopt_args_tail_block_args_tailf_arg','f_rest_argopt_args_tail_block_args_tailf_argexcessed_commaf_arg','f_rest_arg','f_argopt_args_tail_block_args_tailf_argopt_args_tail_block_args_tailf_optarg_primary_value','f_rest_argopt_args_tail_block_args_tailf_optarg_primary_value','f_rest_arg','f_argopt_args_tail_block_args_tailf_optarg_primary_valueopt_args_tail_block_args_tailf_optarg_primary_value','f_argopt_args_tail_block_args_tailf_rest_argopt_args_tail_block_args_tailf_rest_arg','f_argopt_args_tail_block_args_tailblock_args_tail + +

opt_block_param

noneblock_param_def + +

block_param_def

'|'opt_bv_decl'|''|'block_paramopt_bv_decl'|' + +

opt_bv_decl

option_'\n'option_'\n'';'bv_declsoption_'\n' + +

bv_decls

bvarbv_decls','bvar + +

bvar

tIDENTIFIERf_bad_arg + +

max_numparam

+ +

numparam

+ +

it_id

+ +

@26

+ +

$@27

+ +

lambda

tLAMBDA@26max_numparamnumparamit_idallow_exitsf_larglist$@27lambda_body + +

f_larglist

'('f_argsopt_bv_decl')'f_args + +

lambda_body

tLAMBEGcompstmt_stmts'}'keyword_do_LAMBDA$@28bodystmtk_end + +

$@28

+ +

do_block

k_do_blockdo_bodyk_end + +

block_call

commanddo_blockblock_callcall_op2operation2opt_paren_argsblock_callcall_op2operation2opt_paren_argsbrace_blockblock_callcall_op2operation2command_argsdo_block + +

method_call

fcallparen_argsprimary_valuecall_opoperation2opt_paren_argsprimary_valuetCOLON2operation2paren_argsprimary_valuetCOLON2operation3primary_valuecall_opparen_argsprimary_valuetCOLON2paren_argskeyword_superparen_argskeyword_superprimary_value'['opt_call_argsrbracket + +

brace_block

'{'brace_body'}'k_dodo_bodyk_end + +

@29

+ +

brace_body

@29max_numparamnumparamit_idallow_exitsopt_block_paramcompstmt_stmts + +

@30

+ +

do_body

@30max_numparamnumparamit_idallow_exitsopt_block_parambodystmt + +

case_args

arg_valuetSTARarg_valuecase_args','arg_valuecase_args','tSTARarg_value + +

case_body

k_whencase_argsthencompstmt_stmtscases + +

cases

opt_elsecase_body + +

p_pvtbl

+ +

p_pktbl

+ +

p_in_kwarg

+ +

$@31

+ +

p_case_body

keyword_inp_in_kwargp_pvtblp_pktblp_top_exprthen$@31compstmt_stmtsp_cases + +

p_cases

opt_elsep_case_body + +

p_top_expr

p_top_expr_bodyp_top_expr_bodymodifier_ifexpr_valuep_top_expr_bodymodifier_unlessexpr_value + +

p_top_expr_body

p_exprp_expr','p_expr','p_argsp_findp_args_tailp_kwargs + +

p_expr

p_as + +

p_as

p_exprtASSOCp_variablep_alt + +

p_alt

p_alt'|'p_expr_basicp_expr_basic + +

p_lparen

'('p_pktbl + +

p_lbracket

'['p_pktbl + +

p_expr_basic

p_valuep_variablep_constp_lparenp_argsrparenp_constp_lparenp_findrparenp_constp_lparenp_kwargsrparenp_const'('rparenp_constp_lbracketp_argsrbracketp_constp_lbracketp_findrbracketp_constp_lbracketp_kwargsrbracketp_const'['rbrackettLBRACKp_argsrbrackettLBRACKp_findrbrackettLBRACKrbrackettLBRACEp_pktbllex_ctxt$@32p_kwargsrbracetLBRACErbracetLPARENp_pktblp_exprrparen + +

$@32

+ +

p_args

p_exprp_args_headp_args_headp_argp_args_headp_restp_args_headp_rest','p_args_postp_args_tail + +

p_args_head

p_arg','p_args_headp_arg',' + +

p_args_tail

p_restp_rest','p_args_post + +

p_find

p_rest','p_args_post','p_rest + +

p_rest

tSTARtIDENTIFIERtSTAR + +

p_args_post

p_argp_args_post','p_arg + +

p_arg

p_expr + +

p_kwargs

p_kwarg','p_any_kwrestp_kwargp_kwarg','p_any_kwrest + +

p_kwarg

p_kwp_kwarg','p_kw + +

p_kw

p_kw_labelp_exprp_kw_label + +

p_kw_label

tLABELtSTRING_BEGstring_contentstLABEL_END + +

p_kwrest

kwrest_marktIDENTIFIERkwrest_mark + +

p_kwnorest

kwrest_markkeyword_nil + +

p_any_kwrest

p_kwrestp_kwnorest + +

p_value

p_primitivep_primitive_valuetDOT2p_primitive_valuep_primitive_valuetDOT3p_primitive_valuep_primitive_valuetDOT2p_primitive_valuetDOT3p_var_refp_expr_refp_consttBDOT2p_primitive_valuetBDOT3p_primitive_value + +

p_primitive

literalstringsxstringregexpwordsqwordssymbolsqsymbolskeyword_variablelambda + +

value_expr_p_primitive

p_primitive + +

p_primitive_value

value_expr_p_primitive + +

p_variable

tIDENTIFIER + +

p_var_ref

'^'tIDENTIFIER'^'nonlocal_var + +

p_expr_ref

'^'tLPARENexpr_valuerparen + +

p_const

tCOLON3cnamep_consttCOLON2cnametCONSTANT + +

opt_rescue

k_rescueexc_listexc_varthencompstmt_stmtsopt_rescuenone + +

exc_list

arg_valuemrhsnone + +

exc_var

tASSOClhsnone + +

opt_ensure

k_ensurestmtsoption_termsnone + +

literal

numericsymbol + +

strings

string + +

string

tCHARstring1stringstring1 + +

string1

tSTRING_BEGstring_contentstSTRING_END + +

xstring

tXSTRING_BEGxstring_contentstSTRING_END + +

regexp

tREGEXP_BEGregexp_contentstREGEXP_END + +

nonempty_list_' '

' 'nonempty_list_' '' ' + +

words_tWORDS_BEG_word_list

tWORDS_BEGnonempty_list_' 'word_listtSTRING_END + +

words

words_tWORDS_BEG_word_list + +

word_list

word_listwordnonempty_list_' ' + +

word

string_contentwordstring_content + +

words_tSYMBOLS_BEG_symbol_list

tSYMBOLS_BEGnonempty_list_' 'symbol_listtSTRING_END + +

symbols

words_tSYMBOLS_BEG_symbol_list + +

symbol_list

symbol_listwordnonempty_list_' ' + +

words_tQWORDS_BEG_qword_list

tQWORDS_BEGnonempty_list_' 'qword_listtSTRING_END + +

qwords

words_tQWORDS_BEG_qword_list + +

words_tQSYMBOLS_BEG_qsym_list

tQSYMBOLS_BEGnonempty_list_' 'qsym_listtSTRING_END + +

qsymbols

words_tQSYMBOLS_BEG_qsym_list + +

qword_list

qword_listtSTRING_CONTENTnonempty_list_' ' + +

qsym_list

qsym_listtSTRING_CONTENTnonempty_list_' ' + +

string_contents

string_contentsstring_content + +

xstring_contents

xstring_contentsstring_content + +

regexp_contents

regexp_contentsstring_content + +

string_content

tSTRING_CONTENTtSTRING_DVAR@33string_dvartSTRING_DBEG@34@35@36compstmt_stmtsstring_dend + +

@33

+ +

@34

+ +

@35

+ +

@36

+ +

string_dend

tSTRING_DENDEND_OF_INPUT + +

string_dvar

nonlocal_varbackref + +

symbol

ssymdsym + +

ssym

tSYMBEGsym + +

sym

fnamenonlocal_var + +

dsym

tSYMBEGstring_contentstSTRING_END + +

numeric

simple_numerictUMINUS_NUMsimple_numeric + +

simple_numeric

tINTEGERtFLOATtRATIONALtIMAGINARY + +

nonlocal_var

tIVARtGVARtCVAR + +

user_variable

tIDENTIFIERtCONSTANTnonlocal_var + +

keyword_variable

keyword_nilkeyword_selfkeyword_truekeyword_falsekeyword__FILE__keyword__LINE__keyword__ENCODING__ + +

var_ref

user_variablekeyword_variable + +

var_lhs

user_variablekeyword_variable + +

backref

tNTH_REFtBACK_REF + +

$@37

+ +

superclass

'<'$@37expr_valueterm + +

f_opt_paren_args

f_paren_argsnone + +

f_paren_args

'('f_argsrparen + +

f_arglist

f_paren_args@38f_argsterm + +

@38

+ +

f_kw_arg_value

f_labelarg_valuef_label + +

f_kwarg_arg_value

f_kw_arg_valuef_kwarg_arg_value','f_kw_arg_value + +

args_tail

f_kwarg_arg_value','f_kwrestopt_f_block_argf_kwarg_arg_valueopt_f_block_argf_any_kwrestopt_f_block_argf_block_argargs_forward + +

f_opt_arg_value

f_arg_asgnf_eqarg_value + +

f_optarg_arg_value

f_opt_arg_valuef_optarg_arg_value','f_opt_arg_value + +

opt_args_tail_args_tail

','args_tail + +

f_args

f_arg','f_optarg_arg_value','f_rest_argopt_args_tail_args_tailf_arg','f_optarg_arg_value','f_rest_arg','f_argopt_args_tail_args_tailf_arg','f_optarg_arg_valueopt_args_tail_args_tailf_arg','f_optarg_arg_value','f_argopt_args_tail_args_tailf_arg','f_rest_argopt_args_tail_args_tailf_arg','f_rest_arg','f_argopt_args_tail_args_tailf_argopt_args_tail_args_tailf_optarg_arg_value','f_rest_argopt_args_tail_args_tailf_optarg_arg_value','f_rest_arg','f_argopt_args_tail_args_tailf_optarg_arg_valueopt_args_tail_args_tailf_optarg_arg_value','f_argopt_args_tail_args_tailf_rest_argopt_args_tail_args_tailf_rest_arg','f_argopt_args_tail_args_tailargs_tail + +

args_forward

tBDOT3 + +

f_bad_arg

tCONSTANTtIVARtGVARtCVAR + +

f_norm_arg

f_bad_argtIDENTIFIER + +

f_arg_asgn

f_norm_arg + +

f_arg_item

f_arg_asgntLPARENf_margsrparen + +

f_arg

f_arg_itemf_arg','f_arg_item + +

f_label

tLABEL + +

kwrest_mark

tPOWtDSTAR + +

f_no_kwarg

p_kwnorest + +

f_kwrest

kwrest_marktIDENTIFIERkwrest_mark + +

restarg_mark

'*'tSTAR + +

f_rest_arg

restarg_marktIDENTIFIERrestarg_mark + +

blkarg_mark

'&'tAMPER + +

f_block_arg

blkarg_marktIDENTIFIERblkarg_mark + +

opt_f_block_arg

','f_block_argnone + +

value_expr_var_ref

var_ref + +

singleton

value_expr_var_ref'('$@39exprrparen + +

$@39

+ +

assoc_list

noneassocstrailer + +

assocs

assocassocs','assoc + +

assoc

arg_valuetASSOCarg_valuetLABELarg_valuetLABELtSTRING_BEGstring_contentstLABEL_ENDarg_valuetDSTARarg_valuetDSTAR + +

operation

tIDENTIFIERtCONSTANTtFID + +

operation2

operationop + +

operation3

tIDENTIFIERtFIDop + +

dot_or_colon

'.'tCOLON2 + +

call_op

'.'tANDDOT + +

call_op2

call_optCOLON2 + +

rparen

option_'\n'')' + +

rbracket

option_'\n'']' + +

rbrace

option_'\n''}' + +

trailer

option_'\n'',' + +

term

';''\n' + +

terms

termterms';' + +

none

+ + diff --git a/sig/lrama/grammar/rule.rbs b/sig/lrama/grammar/rule.rbs index 53c1a001..a5f2049c 100644 --- a/sig/lrama/grammar/rule.rbs +++ b/sig/lrama/grammar/rule.rbs @@ -36,6 +36,8 @@ module Lrama def contains_at_reference?: -> bool + def rhs_to_diagram: -> Array[(RailroadDiagrams::Terminal | RailroadDiagrams::NonTerminal)] + interface _DelegatedMethods def lhs: -> Grammar::Symbol def rhs: -> Array[Grammar::Symbol] diff --git a/sig/railroad_diagrams/railroad_diagrams.rbs b/sig/railroad_diagrams/railroad_diagrams.rbs new file mode 100644 index 00000000..d39e532d --- /dev/null +++ b/sig/railroad_diagrams/railroad_diagrams.rbs @@ -0,0 +1,14 @@ +module RailroadDiagrams + class Terminal + def initialize: (*untyped) -> void + end + class NonTerminal + def initialize: (*untyped) -> void + end + class Sequence + def initialize: (*untyped) -> void + end + class Skip + def initialize: (*untyped) -> void + end +end diff --git a/spec/lrama/diagram_spec.rb b/spec/lrama/diagram_spec.rb new file mode 100644 index 00000000..c6113507 --- /dev/null +++ b/spec/lrama/diagram_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +RSpec.describe Lrama::Diagram do + let(:diagram) { + Lrama::Diagram.new( + out: out, + grammar: grammar, + template_name: "diagram/diagram.html", + ) + } + let(:out) { StringIO.new } + let(:grammar_file_path) { fixture_path("common/basic.y") } + let(:text) { File.read(grammar_file_path) } + let(:grammar) do + grammar = Lrama::Parser.new(text, grammar_file_path).parse + grammar.prepare + grammar.validate! + grammar + end + + + describe "#default_style" do + it "returns the default style" do + expect(diagram.default_style).to eq RailroadDiagrams::Style::default_style + end + end + + describe "#diagrams" do + it "returns diagrams" do + expect(diagram.diagrams).to include("

$accept

") + expect(diagram.diagrams).to include("

unused

") + expect(diagram.diagrams).to include(" + + + Lrama syntax diagrams + + + + + + + <%= output.diagrams %> + From 0328a3d93b427887733f48ba3767b2adac0b1df4 Mon Sep 17 00:00:00 2001 From: ydah Date: Sat, 1 Feb 2025 21:01:46 +0900 Subject: [PATCH 2/7] Update Gemfile to specify railroad_diagrams version 0.2.1 --- Gemfile | 5 +---- template/diagram/diagram.html | 18 ------------------ 2 files changed, 1 insertion(+), 22 deletions(-) diff --git a/Gemfile b/Gemfile index 8b73c600..f18d4f8c 100644 --- a/Gemfile +++ b/Gemfile @@ -12,10 +12,7 @@ gem "rspec" gem "simplecov", require: false gem "stackprof", platforms: [:ruby] # stackprof doesn't support Windows gem "memory_profiler" - -if RUBY_VERSION >= "3.1.0" - gem "railroad_diagrams" -end +gem "railroad_diagrams", "0.2.1" # Recent steep requires Ruby >= 3.0.0. # Then skip install on some CI jobs. diff --git a/template/diagram/diagram.html b/template/diagram/diagram.html index 5f2dd329..837068ee 100644 --- a/template/diagram/diagram.html +++ b/template/diagram/diagram.html @@ -5,24 +5,6 @@ From 91c587924cbda67324cd3438f563e7086e8b0730 Mon Sep 17 00:00:00 2001 From: ydah Date: Sat, 1 Feb 2025 21:06:25 +0900 Subject: [PATCH 3/7] Add error handling for missing railroad_diagrams gem --- lib/lrama/diagram.rb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/lrama/diagram.rb b/lib/lrama/diagram.rb index 52aee680..0ad435ce 100644 --- a/lib/lrama/diagram.rb +++ b/lib/lrama/diagram.rb @@ -1,7 +1,11 @@ # frozen_string_literal: true require "erb" -require "railroad_diagrams" +begin + require "railroad_diagrams" +rescue LoadError + warn "railroad_diagrams is not installed. Please run `bundle install`." +end module Lrama class Diagram @@ -9,6 +13,7 @@ def initialize(out:, grammar:, template_name: 'diagram/diagram.html') @grammar = grammar @out = out @template_name = template_name + return unless defined?(RailroadDiagrams) # Skip rendering if railroad_diagrams is not installed RailroadDiagrams::TextDiagram.set_formatting(RailroadDiagrams::TextDiagram::PARTS_UNICODE) end @@ -23,6 +28,7 @@ def self.erb(input) end def render + return unless defined?(RailroadDiagrams) # Skip rendering if railroad_diagrams is not installed @out << render_template(template_file) end From 78da5158e255a17ce856071310496981b9d030f8 Mon Sep 17 00:00:00 2001 From: ydah Date: Sat, 1 Feb 2025 21:52:34 +0900 Subject: [PATCH 4/7] Remove bundler-cache option from GitHub Actions workflow --- .github/workflows/test.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 2d02739a..35f6e00f 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -26,7 +26,6 @@ jobs: - uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} - bundler-cache: true - run: flex --help - run: bundle install - run: bundle exec rspec From ffd311599a114ad69f12f2d619978facbda20a5d Mon Sep 17 00:00:00 2001 From: ydah Date: Mon, 3 Feb 2025 15:28:34 +0900 Subject: [PATCH 5/7] Add comment for delete comment reason --- .github/workflows/test.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 35f6e00f..b7810b69 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -26,6 +26,9 @@ jobs: - uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} + # 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 From 4280ca2aac11b7173a94ba95e03d827483ba2bd8 Mon Sep 17 00:00:00 2001 From: ydah Date: Mon, 3 Feb 2025 17:20:48 +0900 Subject: [PATCH 6/7] Add usage instructions for diagram option in NEWS.md --- NEWS.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/NEWS.md b/NEWS.md index e60df4ba..723394ca 100644 --- a/NEWS.md +++ b/NEWS.md @@ -8,6 +8,12 @@ Lrama provides an API for generating HTML syntax diagrams. These visual diagrams ![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 From 9113e0b873664f7eb1306ded809474e418251967 Mon Sep 17 00:00:00 2001 From: ydah Date: Mon, 3 Feb 2025 17:45:30 +0900 Subject: [PATCH 7/7] Extract require logic to an instance method and call the instance method when it's necessary, it might be needed when `Diagram#initialize` is called --- lib/lrama/command.rb | 2 +- lib/lrama/diagram.rb | 24 ++++++++++++------- spec/lrama/diagram_spec.rb | 47 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 64 insertions(+), 9 deletions(-) diff --git a/lib/lrama/command.rb b/lib/lrama/command.rb index 9a02e1be..31d6be55 100644 --- a/lib/lrama/command.rb +++ b/lib/lrama/command.rb @@ -49,7 +49,7 @@ def run(argv) if options.diagram File.open(options.diagram_file, "w+") do |f| - Lrama::Diagram.new(out: f, grammar: grammar).render + Lrama::Diagram.render(out: f, grammar: grammar) end end diff --git a/lib/lrama/diagram.rb b/lib/lrama/diagram.rb index 0ad435ce..28679f55 100644 --- a/lib/lrama/diagram.rb +++ b/lib/lrama/diagram.rb @@ -1,20 +1,28 @@ # frozen_string_literal: true require "erb" -begin - require "railroad_diagrams" -rescue LoadError - warn "railroad_diagrams is not installed. Please run `bundle install`." -end 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 - return unless defined?(RailroadDiagrams) # Skip rendering if railroad_diagrams is not installed - RailroadDiagrams::TextDiagram.set_formatting(RailroadDiagrams::TextDiagram::PARTS_UNICODE) end if ERB.instance_method(:initialize).parameters.last.first == :key @@ -28,7 +36,7 @@ def self.erb(input) end def render - return unless defined?(RailroadDiagrams) # Skip rendering if railroad_diagrams is not installed + RailroadDiagrams::TextDiagram.set_formatting(RailroadDiagrams::TextDiagram::PARTS_UNICODE) @out << render_template(template_file) end diff --git a/spec/lrama/diagram_spec.rb b/spec/lrama/diagram_spec.rb index c6113507..a5c5fece 100644 --- a/spec/lrama/diagram_spec.rb +++ b/spec/lrama/diagram_spec.rb @@ -18,14 +18,61 @@ grammar end + describe ".render" do + it "renders a diagram" do + expect { Lrama::Diagram.render(out: out, grammar: grammar) }.not_to raise_error + expect(out.string).to include("

$accept

") + expect(out.string).to include("

unused

") + expect(out.string).to include("$accept") + expect(out.string).to include("

unused

") + expect(out.string).to include("$accept") expect(diagram.diagrams).to include("

unused

")