Show grammar line numbers for regex errors

This commit is contained in:
Josh Holtrop 2025-07-25 21:27:01 -04:00
parent 25d6e3bc34
commit 5b243507cf
4 changed files with 46 additions and 33 deletions

View File

@ -55,7 +55,7 @@ class Propane
@line_number = options[:line_number]
@modes = options[:modes]
@ptypename = options[:ptypename]
regex = Regex.new(@pattern)
regex = Regex.new(@pattern, @line_number)
regex.nfa.end_state.accepts = self
@nfa = regex.nfa
end

View File

@ -4,12 +4,13 @@ class Propane
attr_reader :unit
attr_reader :nfa
def initialize(pattern)
def initialize(pattern, line_number)
@pattern = pattern.dup
@line_number = line_number
@unit = parse_alternates
@nfa = @unit.to_nfa
if @pattern != ""
raise Error.new(%[Unexpected "#{@pattern}" in pattern])
raise Error.new(%[Line #{@line_number}: unexpected "#{@pattern}" in pattern])
end
end
@ -41,7 +42,7 @@ class Propane
mu = MultiplicityUnit.new(last_unit, min_count, max_count)
au.replace_last!(mu)
else
raise Error.new("#{c} follows nothing")
raise Error.new("Line #{@line_number}: #{c} follows nothing")
end
when "|"
au.new_alternate!
@ -59,7 +60,7 @@ class Propane
def parse_group
au = parse_alternates
if @pattern[0] != ")"
raise Error.new("Unterminated group in pattern")
raise Error.new("Line #{@line_number}: unterminated group in pattern")
end
@pattern.slice!(0)
au
@ -70,7 +71,7 @@ class Propane
index = 0
loop do
if @pattern == ""
raise Error.new("Unterminated character class")
raise Error.new("Line #{@line_number}: unterminated character class")
end
c = @pattern.slice!(0)
if c == "]"
@ -84,13 +85,13 @@ class Propane
elsif c == "-" && @pattern[0] != "]"
begin_cu = ccu.last_unit
unless begin_cu.is_a?(CharacterRangeUnit) && begin_cu.code_point_range.size == 1
raise Error.new("Character range must be between single characters")
raise Error.new("Line #{@line_number}: character range must be between single characters")
end
if @pattern[0] == "\\"
@pattern.slice!(0)
end_cu = parse_backslash
unless end_cu.is_a?(CharacterRangeUnit) && end_cu.code_point_range.size == 1
raise Error.new("Character range must be between single characters")
raise Error.new("Line #{@line_number}: character range must be between single characters")
end
max_code_point = end_cu.code_point
else
@ -116,7 +117,7 @@ class Propane
elsif max_count.to_s != ""
max_count = max_count.to_i
if max_count < min_count
raise Error.new("Maximum repetition count cannot be less than minimum repetition count")
raise Error.new("Line #{@line_number}: maximum repetition count cannot be less than minimum repetition count")
end
else
max_count = nil
@ -124,13 +125,13 @@ class Propane
@pattern = pattern
[min_count, max_count]
else
raise Error.new("Unexpected match count at #{@pattern}")
raise Error.new("Line #{@line_number}: unexpected match count following {")
end
end
def parse_backslash
if @pattern == ""
raise Error.new("Error: unfollowed \\")
raise Error.new("Line #{@line_number}: error: unfollowed \\")
else
c = @pattern.slice!(0)
case c

View File

@ -2,14 +2,14 @@ class Propane
RSpec.describe Regex do
it "parses an empty expression" do
regex = Regex.new("")
regex = Regex.new("", 1)
expect(regex.unit).to be_a Regex::AlternatesUnit
expect(regex.unit.alternates.size).to eq 1
expect(regex.unit.alternates[0].size).to eq 0
end
it "parses a single character unit expression" do
regex = Regex.new("a")
regex = Regex.new("a", 1)
expect(regex.unit).to be_a Regex::AlternatesUnit
expect(regex.unit.alternates.size).to eq 1
expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit
@ -19,7 +19,7 @@ class Propane
end
it "parses a group with a single character unit expression" do
regex = Regex.new("(a)")
regex = Regex.new("(a)", 1)
expect(regex.unit).to be_a Regex::AlternatesUnit
expect(regex.unit.alternates.size).to eq 1
expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit
@ -33,7 +33,7 @@ class Propane
end
it "parses a *" do
regex = Regex.new("a*")
regex = Regex.new("a*", 1)
expect(regex.unit).to be_a Regex::AlternatesUnit
expect(regex.unit.alternates.size).to eq 1
expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit
@ -47,7 +47,7 @@ class Propane
end
it "parses a +" do
regex = Regex.new("a+")
regex = Regex.new("a+", 1)
expect(regex.unit).to be_a Regex::AlternatesUnit
expect(regex.unit.alternates.size).to eq 1
expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit
@ -61,7 +61,7 @@ class Propane
end
it "parses a ?" do
regex = Regex.new("a?")
regex = Regex.new("a?", 1)
expect(regex.unit).to be_a Regex::AlternatesUnit
expect(regex.unit.alternates.size).to eq 1
expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit
@ -75,7 +75,7 @@ class Propane
end
it "parses a multiplicity count" do
regex = Regex.new("a{5}")
regex = Regex.new("a{5}", 1)
expect(regex.unit).to be_a Regex::AlternatesUnit
expect(regex.unit.alternates.size).to eq 1
expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit
@ -89,7 +89,7 @@ class Propane
end
it "parses a minimum-only multiplicity count" do
regex = Regex.new("a{5,}")
regex = Regex.new("a{5,}", 1)
expect(regex.unit).to be_a Regex::AlternatesUnit
expect(regex.unit.alternates.size).to eq 1
expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit
@ -103,7 +103,7 @@ class Propane
end
it "parses a minimum and maximum multiplicity count" do
regex = Regex.new("a{5,8}")
regex = Regex.new("a{5,8}", 1)
expect(regex.unit).to be_a Regex::AlternatesUnit
expect(regex.unit.alternates.size).to eq 1
expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit
@ -118,7 +118,7 @@ class Propane
end
it "parses an escaped *" do
regex = Regex.new("a\\*")
regex = Regex.new("a\\*", 1)
expect(regex.unit).to be_a Regex::AlternatesUnit
expect(regex.unit.alternates.size).to eq 1
expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit
@ -131,7 +131,7 @@ class Propane
end
it "parses an escaped +" do
regex = Regex.new("a\\+")
regex = Regex.new("a\\+", 1)
expect(regex.unit).to be_a Regex::AlternatesUnit
expect(regex.unit.alternates.size).to eq 1
expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit
@ -144,7 +144,7 @@ class Propane
end
it "parses an escaped \\" do
regex = Regex.new("\\\\d")
regex = Regex.new("\\\\d", 1)
expect(regex.unit).to be_a Regex::AlternatesUnit
expect(regex.unit.alternates.size).to eq 1
expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit
@ -157,7 +157,7 @@ class Propane
end
it "parses a character class" do
regex = Regex.new("[a-z_]")
regex = Regex.new("[a-z_]", 1)
expect(regex.unit).to be_a Regex::AlternatesUnit
expect(regex.unit.alternates.size).to eq 1
expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit
@ -175,7 +175,7 @@ class Propane
end
it "parses a negated character class" do
regex = Regex.new("[^xyz]")
regex = Regex.new("[^xyz]", 1)
expect(regex.unit).to be_a Regex::AlternatesUnit
expect(regex.unit.alternates.size).to eq 1
expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit
@ -207,7 +207,7 @@ class Propane
end
it "parses - as a plain character at beginning of a character class" do
regex = Regex.new("[-9]")
regex = Regex.new("[-9]", 1)
expect(regex.unit).to be_a Regex::AlternatesUnit
expect(regex.unit.alternates.size).to eq 1
expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit
@ -221,7 +221,7 @@ class Propane
end
it "parses - as a plain character at end of a character class" do
regex = Regex.new("[0-]")
regex = Regex.new("[0-]", 1)
expect(regex.unit).to be_a Regex::AlternatesUnit
expect(regex.unit.alternates.size).to eq 1
expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit
@ -237,7 +237,7 @@ class Propane
end
it "parses - as a plain character at beginning of a negated character class" do
regex = Regex.new("[^-9]")
regex = Regex.new("[^-9]", 1)
expect(regex.unit).to be_a Regex::AlternatesUnit
expect(regex.unit.alternates.size).to eq 1
expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit
@ -252,7 +252,7 @@ class Propane
end
it "parses . as a plain character in a character class" do
regex = Regex.new("[.]")
regex = Regex.new("[.]", 1)
expect(regex.unit).to be_a Regex::AlternatesUnit
expect(regex.unit.alternates.size).to eq 1
expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit
@ -267,7 +267,7 @@ class Propane
end
it "parses - as a plain character when escaped in middle of character class" do
regex = Regex.new("[0\\-9]")
regex = Regex.new("[0\\-9]", 1)
expect(regex.unit).to be_a Regex::AlternatesUnit
expect(regex.unit.alternates.size).to eq 1
expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit
@ -286,7 +286,7 @@ class Propane
end
it "parses alternates" do
regex = Regex.new("ab|c")
regex = Regex.new("ab|c", 1)
expect(regex.unit).to be_a Regex::AlternatesUnit
expect(regex.unit.alternates.size).to eq 2
expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit
@ -296,7 +296,7 @@ class Propane
end
it "parses a ." do
regex = Regex.new("a.b")
regex = Regex.new("a.b", 1)
expect(regex.unit).to be_a Regex::AlternatesUnit
expect(regex.unit.alternates.size).to eq 1
expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit
@ -307,7 +307,7 @@ class Propane
end
it "parses something complex" do
regex = Regex.new("(a|)*|[^^]|\\|v|[x-y]+")
regex = Regex.new("(a|)*|[^^]|\\|v|[x-y]+", 1)
expect(regex.unit).to be_a Regex::AlternatesUnit
expect(regex.unit.alternates.size).to eq 4
expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit

View File

@ -249,6 +249,18 @@ EOF
expect(results.status).to eq 0
end
it "shows error line number for unmatched left curly brace" do
write_grammar <<EOF
# Line 1
# Line 2
token a /a{/;
Start -> a;
EOF
results = run_propane(capture: true)
expect(results.stderr).to match /Line 3: unexpected match count following \{/
expect(results.status).to_not eq 0
end
%w[d c].each do |language|
context "#{language.upcase} language" do