1+ package peggysue
2+
3+ import (
4+ "fmt"
5+ "strings"
6+ )
7+
8+ // ParseError represents a detailed parse failure with position and expectation information
9+ type ParseError struct {
10+ Input string
11+ MaxPos int
12+ Failures []RuleFailure
13+ filename string
14+ linePos []int
15+ }
16+
17+ // RuleFailure represents a single rule that failed to match at a specific position
18+ type RuleFailure struct {
19+ Pos int
20+ Rule Rule
21+ Expected string // Human-readable expectation
22+ }
23+
24+ // Error implements the error interface
25+ func (e * ParseError ) Error () string {
26+ if len (e .Failures ) == 0 {
27+ return fmt .Sprintf ("parse failed at position %d" , e .MaxPos )
28+ }
29+
30+ // Find the furthest failures for the most relevant error
31+ furthest := e .getFurthestFailures ()
32+
33+ // Use the actual failure position, not necessarily MaxPos
34+ errorPos := e .MaxPos
35+ if len (furthest ) > 0 {
36+ errorPos = furthest [0 ].Pos
37+ }
38+
39+ line , col := e .lineCol (errorPos )
40+ preview := e .getPreview (errorPos )
41+
42+ var expectations []string
43+ seen := make (map [string ]bool )
44+
45+ for _ , f := range furthest {
46+ exp := f .Expected
47+ if exp == "" {
48+ exp = Print (f .Rule )
49+ }
50+ if ! seen [exp ] {
51+ seen [exp ] = true
52+ expectations = append (expectations , exp )
53+ }
54+ }
55+
56+ var msg strings.Builder
57+ if e .filename != "" {
58+ fmt .Fprintf (& msg , "%s:%d:%d: " , e .filename , line , col )
59+ } else {
60+ fmt .Fprintf (& msg , "line %d, column %d: " , line , col )
61+ }
62+
63+ fmt .Fprintf (& msg , "expected %s" , formatExpectations (expectations ))
64+
65+ if preview != "" {
66+ fmt .Fprintf (& msg , "\n %s" , preview )
67+ }
68+
69+ return msg .String ()
70+ }
71+
72+ // Position returns the furthest position reached during parsing
73+ func (e * ParseError ) Position () (line , col int ) {
74+ return e .lineCol (e .MaxPos )
75+ }
76+
77+ // getFurthestFailures returns failures at the maximum position
78+ func (e * ParseError ) getFurthestFailures () []RuleFailure {
79+ var furthest []RuleFailure
80+ for _ , f := range e .Failures {
81+ if f .Pos == e .MaxPos {
82+ furthest = append (furthest , f )
83+ }
84+ }
85+ return furthest
86+ }
87+
88+ // lineCol converts a byte position to line and column numbers
89+ func (e * ParseError ) lineCol (pos int ) (line , col int ) {
90+ line = 1
91+ lastNewline := - 1
92+
93+ for _ , nlPos := range e .linePos {
94+ if nlPos >= pos {
95+ break
96+ }
97+ line ++
98+ lastNewline = nlPos
99+ }
100+
101+ col = pos - lastNewline
102+ return line , col
103+ }
104+
105+ // getPreview returns a preview of the input around the error position
106+ func (e * ParseError ) getPreview (pos int ) string {
107+ if pos >= len (e .Input ) {
108+ pos = len (e .Input ) - 1
109+ }
110+ if pos < 0 {
111+ return ""
112+ }
113+
114+ // Find start of line
115+ start := pos
116+ for start > 0 && e .Input [start - 1 ] != '\n' {
117+ start --
118+ }
119+
120+ // Find end of line
121+ end := pos
122+ for end < len (e .Input ) && e .Input [end ] != '\n' {
123+ end ++
124+ }
125+
126+ if start >= end {
127+ return ""
128+ }
129+
130+ line := e .Input [start :end ]
131+ pointer := strings .Repeat (" " , pos - start ) + "^"
132+
133+ return fmt .Sprintf (" %s\n %s" , line , pointer )
134+ }
135+
136+ // formatExpectations formats a list of expectations into a readable string
137+ func formatExpectations (expectations []string ) string {
138+ switch len (expectations ) {
139+ case 0 :
140+ return "valid input"
141+ case 1 :
142+ return expectations [0 ]
143+ case 2 :
144+ return expectations [0 ] + " or " + expectations [1 ]
145+ default :
146+ return strings .Join (expectations [:len (expectations )- 1 ], ", " ) + ", or " + expectations [len (expectations )- 1 ]
147+ }
148+ }
149+
150+ // ParseOption configures parsing behavior
151+ type ParseOption func (* parseConfig )
152+
153+ type parseConfig struct {
154+ collectErrors bool
155+ partialOk bool
156+ debug bool
157+ filename string
158+ }
159+
160+ // WithErrors enables detailed error collection during parsing
161+ func WithErrors () ParseOption {
162+ return func (c * parseConfig ) {
163+ c .collectErrors = true
164+ }
165+ }
166+
167+ // WithPartialMatch allows partial matching (doesn't require full input consumption)
168+ func WithPartialMatch () ParseOption {
169+ return func (c * parseConfig ) {
170+ c .partialOk = true
171+ }
172+ }
173+
174+ // WithFilename sets the filename for error reporting
175+ func WithFilename (filename string ) ParseOption {
176+ return func (c * parseConfig ) {
177+ c .filename = filename
178+ }
179+ }
180+
181+ // Parse attempts to match the given rule against the input string with options.
182+ // By default, no error tracking is performed for backward compatibility.
183+ // Use WithErrors() to enable detailed error reporting.
184+ func Parse (r Rule , input string , opts ... ParseOption ) (val any , matched bool , err error ) {
185+ config := parseConfig {
186+ collectErrors : false , // default to no error collection for backward compatibility
187+ partialOk : false , // default to full match required
188+ }
189+
190+ for _ , opt := range opts {
191+ opt (& config )
192+ }
193+
194+ s := & state {
195+ input : input ,
196+ inputSize : len (input ),
197+ pos : 0 ,
198+ maxPos : 0 ,
199+ filename : config .filename ,
200+ collectErrors : config .collectErrors ,
201+ linePos : computeLines (input ),
202+ }
203+
204+ if config .collectErrors {
205+ s .failures = make ([]RuleFailure , 0 )
206+ }
207+
208+ s .setupBadGood (config .debug )
209+
210+ res := s .match (r )
211+
212+ if ! res .matched {
213+ if config .collectErrors && len (s .failures ) > 0 {
214+ return nil , false , & ParseError {
215+ Input : input ,
216+ MaxPos : s .maxPos ,
217+ Failures : s .failures ,
218+ filename : config .filename ,
219+ linePos : s .linePos ,
220+ }
221+ }
222+ return nil , false , nil
223+ }
224+
225+ if ! config .partialOk && s .pos != len (input ) {
226+ if config .collectErrors {
227+ return res .value , false , & ParseError {
228+ Input : input ,
229+ MaxPos : s .pos ,
230+ Failures : []RuleFailure {{Pos : s .pos , Expected : "end of input" }},
231+ filename : config .filename ,
232+ linePos : s .linePos ,
233+ }
234+ }
235+
236+ // Maintain backward compatibility with existing error
237+ return res .value , false , & ErrInputNotConsumed {
238+ MaxPos : s .maxPos ,
239+ MaxRule : s .maxRule ,
240+ }
241+ }
242+
243+ return res .value , true , nil
244+ }
0 commit comments