From 7a140623ff86a752126695fa762109f3abeb5c9d Mon Sep 17 00:00:00 2001 From: Josh Holtrop Date: Thu, 21 May 2026 16:24:25 -0700 Subject: [PATCH] Use #line for user code blocks to report input grammar position for errors --- lib/propane.rb | 2 +- lib/propane/generator.rb | 8 +++++++- lib/propane/grammar.rb | 20 ++++++++++++++------ spec/propane/grammar_spec.rb | 18 +++++++++--------- spec/propane/lexer/dfa_spec.rb | 2 +- 5 files changed, 32 insertions(+), 18 deletions(-) diff --git a/lib/propane.rb b/lib/propane.rb index 715c706..9bb5683 100644 --- a/lib/propane.rb +++ b/lib/propane.rb @@ -33,7 +33,7 @@ class Propane def run(input_file, output_file, log_file, options) begin - grammar = Grammar.new(File.read(input_file)) + grammar = Grammar.new(File.read(input_file), input_file) generator = Generator.new(grammar, output_file, log_file, options) generator.generate rescue Error => e diff --git a/lib/propane/generator.rb b/lib/propane/generator.rb index c20483c..aa1f6e0 100644 --- a/lib/propane/generator.rb +++ b/lib/propane/generator.rb @@ -38,7 +38,13 @@ class Propane output_file = @output_file end erb = ERB.new(template, trim_mode: "<>") - result = erb.result(binding.clone) + result = erb.result(binding.clone).lines.each_with_index.map do |line, i| + if line == "#linereset\n" + "#line #{i + 2}\n" + else + line + end + end.join File.open(output_file, "wb") do |fh| fh.write(result) end diff --git a/lib/propane/grammar.rb b/lib/propane/grammar.rb index f8e6569..ece158b 100644 --- a/lib/propane/grammar.rb +++ b/lib/propane/grammar.rb @@ -22,7 +22,8 @@ class Propane attr_reader :on_token_node attr_reader :token_user_fields - def initialize(input) + def initialize(input, filename) + @filename = filename @patterns = [] @start_rules = [] @tokens = [] @@ -304,8 +305,12 @@ class Propane def parse_code_block_statement! if md = consume!(/<<([a-z]*)(.*?)>>\n/m) name, code = md[1..2] - code.sub!(/\A\n/, "") - code += "\n" unless code.end_with?("\n") + code = code.chomp + if code.start_with?("\n") + code = %[#line #{@line_number + 1} "#{@filename}"#{code}\n#linereset\n] + else + code = %[#line #{@line_number} "#{@filename}"\n#{code}\n#linereset\n] + end if @code_blocks[name] @code_blocks[name] += code else @@ -346,9 +351,12 @@ class Propane def parse_code_block! if md = consume!(/<<(.*?)>>\n/m) - code = md[1] - code.sub!(/\A\n/, "") - code += "\n" unless code.end_with?("\n") + code = md[1].chomp + if code.start_with?("\n") + code = %[#line #{@line_number + 1} "#{@filename}"#{code}\n#linereset\n] + else + code = %[#line #{@line_number} "#{@filename}"\n#{code}\n#linereset\n] + end code end end diff --git a/spec/propane/grammar_spec.rb b/spec/propane/grammar_spec.rb index 2f813fe..47f70a0 100644 --- a/spec/propane/grammar_spec.rb +++ b/spec/propane/grammar_spec.rb @@ -28,7 +28,7 @@ B -> << b = 0; >> EOF - grammar = Grammar.new(input) + grammar = Grammar.new(input, "test.propane") expect(grammar.modulename).to eq "a.b" expect(grammar.ptype).to eq "XYZ *" expect(grammar.ptypes).to eq("default" => "XYZ *") @@ -62,7 +62,7 @@ EOF expect(o).to_not be_nil expect(o.pattern).to eq "token_with_code" expect(o.line_number).to eq 11 - expect(o.code).to eq "Code for the token\n" + expect(o.code).to eq %[#line 12 "test.propane"\nCode for the token\n#linereset\n] o = grammar.tokens.find {|token| token.name == "token_with_no_pattern"} expect(o).to_not be_nil @@ -83,7 +83,7 @@ EOF expect(o.name).to eq "A" expect(o.components).to eq %w[B] expect(o.line_number).to eq 19 - expect(o.code).to eq " a = 42;\n" + expect(o.code).to eq %[#line 20 "test.propane"\n a = 42;\n#linereset\n] o = grammar.rules[1] expect(o.name).to eq "B" @@ -95,7 +95,7 @@ EOF expect(o.name).to eq "B" expect(o.components).to eq [] expect(o.line_number).to eq 23 - expect(o.code).to eq " b = 0;\n" + expect(o.code).to eq %[#line 24 "test.propane"\n b = 0;\n#linereset\n] end it "parses code segments with semicolons" do @@ -113,7 +113,7 @@ tokenid token_with_no_pattern; prefix myparser_; EOF - grammar = Grammar.new(input) + grammar = Grammar.new(input, "test.propane") expect(grammar.prefix).to eq "myparser_" o = grammar.tokens.find {|token| token.name == "code1"} @@ -122,7 +122,7 @@ EOF o = grammar.patterns.find {|pattern| pattern.token == o} expect(o).to_not be_nil - expect(o.code).to eq " a = b;\n return c;\n" + expect(o.code).to eq %[#line 2 "test.propane"\n a = b;\n return c;\n#linereset\n] o = grammar.tokens.find {|token| token.name == "code2"} expect(o).to_not be_nil @@ -130,7 +130,7 @@ EOF o = grammar.patterns.find {|pattern| pattern.token == o} expect(o).to_not be_nil - expect(o.code).to eq %[ writeln("Hello there");\n] + expect(o.code).to eq %[#line 7 "test.propane"\n writeln("Hello there");\n#linereset\n] end it "supports mode labels" do @@ -144,7 +144,7 @@ m2: /bar/ << drop /q/; m3: drop /r/; EOF - grammar = Grammar.new(input) + grammar = Grammar.new(input, "test.propane") o = grammar.tokens.find {|token| token.name == "a"} expect(o).to_not be_nil @@ -197,7 +197,7 @@ tokenid int(integer); Start (node) -> R; R -> abc int; EOF - grammar = Grammar.new(input) + grammar = Grammar.new(input, "test.propane") o = grammar.tokens.find {|token| token.name == "abc"} expect(o).to_not be_nil diff --git a/spec/propane/lexer/dfa_spec.rb b/spec/propane/lexer/dfa_spec.rb index a26a54b..7e9d46e 100644 --- a/spec/propane/lexer/dfa_spec.rb +++ b/spec/propane/lexer/dfa_spec.rb @@ -51,7 +51,7 @@ class TestLexer end def run(grammar, input) - grammar = Propane::Grammar.new(grammar) + grammar = Propane::Grammar.new(grammar, "test.propane") token_dfa = Propane::Lexer::DFA.new(grammar.patterns) test_lexer = TestLexer.new(token_dfa) test_lexer.lex(input)