Skip to content

Commit d4bf2d8

Browse files
committed
Record how far SQLite's parser got, so truncated parses do not lie
The corpus derived "meyer must accept the whole case" from a statement that failed with a semantic message, on the reasoning that it must have parsed to get that far. That is wrong when the message comes from a grammar action in the middle of a statement: sqlite3BeginTrigger raises "no such table" at the trigger_decl reduce, sqlite3RunParser sees pParse->rc set and abandons the loop, and the trigger body is never parsed at all. Three such cases have a deliberate syntax error in the body that SQLite never saw, and the corpus was demanding meyer accept them. sqlite3_prepare_v2's pzTail says exactly how far the parser got: the end of the statement for an ordinary end-of-parse failure, and just past BEGIN for these. The oracle now records it on every failing statement, (*Case).Expected turns short tails into unverified byte ranges, and the harness lets the parser under test fail inside one. 192 of the 4685 cases have such a range; 4 of them were failing. Regenerated the corpus for the added field: the diff is confined to err lines, which gain one integer each. Corpus: 4685/4685 cases passing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JzBeCg7rjweVW3uGPg5G7T
1 parent 6af55df commit d4bf2d8

74 files changed

Lines changed: 6006 additions & 5898 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CLAUDE.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@ stored; the harness derives the expectation from them:
3232
- Otherwise meyer must accept the whole case. This includes statements that
3333
failed **semantically** in SQLite (`no such table: …`) — those parsed
3434
successfully; meyer does no semantic analysis.
35+
- …except in text SQLite's parser never reached. A grammar action can fail
36+
in the middle of a statement — `sqlite3BeginTrigger` raising `no such
37+
table` at the `trigger_decl` reduce, before the trigger body is looked at
38+
— and `sqlite3RunParser` then abandons the rest of the statement. The
39+
oracle records `pzTail` on every failing statement, so the harness knows
40+
which byte ranges are unverified and lets meyer fail inside them.
3541

3642
Known looseness: messages produced by grammar *actions* (e.g. `ORDER BY
3743
clause should come after UNION not before`) are currently classified as

cmd/regenerate-parse/main.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -238,16 +238,19 @@ func parseOracleLine(line string) (testfile.StmtResult, error) {
238238
if fields[2] == "ok" {
239239
return testfile.StmtResult{Offset: off, OK: true}, nil
240240
}
241-
rest := strings.SplitN(strings.TrimPrefix(fields[2], "err "), " ", 3)
242-
if !strings.HasPrefix(fields[2], "err ") || len(rest) != 3 {
241+
rest := strings.SplitN(strings.TrimPrefix(fields[2], "err "), " ", 4)
242+
if !strings.HasPrefix(fields[2], "err ") || len(rest) != 4 {
243243
return testfile.StmtResult{}, fmt.Errorf("malformed oracle line %q", line)
244244
}
245245
rc, err1 := strconv.Atoi(rest[0])
246246
erroff, err2 := strconv.Atoi(rest[1])
247-
if err1 != nil || err2 != nil {
247+
tail, err3 := strconv.Atoi(rest[2])
248+
if err1 != nil || err2 != nil || err3 != nil {
248249
return testfile.StmtResult{}, fmt.Errorf("malformed oracle line %q", line)
249250
}
250-
return testfile.StmtResult{Offset: off, RC: rc, ErrOffset: erroff, Message: rest[2]}, nil
251+
return testfile.StmtResult{
252+
Offset: off, RC: rc, ErrOffset: erroff, Tail: tail, Message: rest[3],
253+
}, nil
251254
}
252255

253256
// mergeMetadata reconciles the sidecar with the regenerated case list:

cmd/regenerate-parse/oracle/oracle.c

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,20 @@
1111
** Output: one line per non-empty statement:
1212
**
1313
** stmt <offset> ok
14-
** stmt <offset> err <rc> <erroff> <message to end of line>
14+
** stmt <offset> err <rc> <erroff> <tail> <message to end of line>
1515
**
1616
** <offset> byte offset of the statement within the input script
1717
** <rc> sqlite3_prepare_v2 result code
1818
** <erroff> sqlite3_error_offset relative to the whole script (-1 if unknown)
19+
** <tail> pzTail relative to the whole script: how far the parser got
20+
**
21+
** <tail> matters because a grammar action can fail in the middle of a
22+
** statement -- sqlite3BeginTrigger raising "no such table" at the
23+
** trigger_decl reduce, say -- and sqlite3RunParser then abandons the rest
24+
** of the statement unparsed. When that happens <tail> stops short of the
25+
** end of the statement, marking the text SQLite never looked at. A
26+
** successful prepare always consumes the whole statement, so "ok" lines
27+
** carry no <tail>.
1928
**
2029
** SQLite error messages never contain newlines, so the line format is safe.
2130
*/
@@ -105,13 +114,17 @@ int main(int argc, char **argv){
105114
stmtText[len] = 0;
106115

107116
sqlite3_stmt *pStmt = 0;
108-
int rc = sqlite3_prepare_v2(db, stmtText, (int)len, &pStmt, 0);
117+
const char *zTail = 0;
118+
int rc = sqlite3_prepare_v2(db, stmtText, (int)len, &pStmt, &zTail);
109119
if( rc==SQLITE_OK ){
110120
printf("stmt %zu ok\n", pos);
111121
}else{
112122
int erroff = sqlite3_error_offset(db);
123+
size_t tail = zTail ? (size_t)(zTail-stmtText) : len;
113124
if( erroff>=0 ) erroff += (int)pos;
114-
printf("stmt %zu err %d %d %s\n", pos, rc, erroff, sqlite3_errmsg(db));
125+
if( tail>len ) tail = len;
126+
printf("stmt %zu err %d %d %zu %s\n", pos, rc, erroff, pos+tail,
127+
sqlite3_errmsg(db));
115128
}
116129
sqlite3_finalize(pStmt);
117130
free(stmtText);

internal/testfile/testfile.go

Lines changed: 62 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,16 @@
66
// <SQL, verbatim, one or more lines>
77
// ----
88
// stmt <offset> ok
9-
// stmt <offset> err <rc> <erroff> <message>
9+
// stmt <offset> err <rc> <erroff> <tail> <message>
1010
//
1111
// The result lines are the raw per-statement output of the SQLite oracle
1212
// (see cmd/regenerate-parse): every statement in the case was prepared
1313
// independently with sqlite3_prepare_v2 against an empty in-memory database.
1414
// Offsets are byte offsets into the case SQL; erroff is -1 when SQLite did
15-
// not report an error position. The corpus stores this raw truth; the
16-
// pass/fail policy for the parser under test is derived from it by
15+
// not report an error position, and tail is how far sqlite3_prepare_v2
16+
// reported having got (a successful prepare always reaches the end of its
17+
// statement, so ok lines carry no tail). The corpus stores this raw truth;
18+
// the pass/fail policy for the parser under test is derived from it by
1719
// (*Case).Expected, so the policy can evolve without regenerating.
1820
//
1921
// Each corpus file has a sidecar <name>.metadata.json:
@@ -37,9 +39,11 @@ import (
3739
// StmtResult is one raw oracle observation for a single statement.
3840
type StmtResult struct {
3941
Offset int // byte offset of the statement within the case SQL
42+
End int // byte offset one past the statement, within the case SQL
4043
OK bool // sqlite3_prepare_v2 returned SQLITE_OK
4144
RC int // prepare result code when !OK
4245
ErrOffset int // sqlite3_error_offset within the case SQL; -1 if unknown
46+
Tail int // pzTail within the case SQL: how far the parser got
4347
Message string // sqlite3_errmsg when !OK
4448
}
4549

@@ -55,6 +59,30 @@ type Expectation struct {
5559
OK bool
5660
Message string // expected parse error message when !OK
5761
Offset int // expected error offset when !OK; -1 if unknown
62+
63+
// Unreached lists byte ranges of the case SQL that SQLite's parser
64+
// never looked at, because a grammar action failed part-way through a
65+
// statement and sqlite3RunParser abandoned the rest of it. The corpus
66+
// says nothing about whether that text is valid SQL, so a parser under
67+
// test may fail inside one of these ranges without being wrong.
68+
Unreached []Range
69+
}
70+
71+
// Range is a half-open byte range of a case's SQL.
72+
type Range struct{ Start, End int }
73+
74+
// IsUnreached reports whether offset falls in text SQLite's parser never
75+
// reached. An offset of -1 (unknown) is never unreached.
76+
func (e Expectation) IsUnreached(offset int) bool {
77+
if offset < 0 {
78+
return false
79+
}
80+
for _, r := range e.Unreached {
81+
if offset >= r.Start && offset < r.End {
82+
return true
83+
}
84+
}
85+
return false
5886
}
5987

6088
// syntaxFamily matches error messages produced by SQLite's parser/tokenizer
@@ -79,14 +107,24 @@ func IsSyntaxError(msg string) bool {
79107

80108
// Expected derives the harness expectation: the first syntax-family error in
81109
// statement order wins (meyer is fail-fast); if there is none, every
82-
// statement must parse.
110+
// statement must parse, except within the ranges SQLite's own parser never
111+
// reached.
83112
func (c *Case) Expected() Expectation {
113+
var unreached []Range
84114
for _, r := range c.Results {
85-
if !r.OK && IsSyntaxError(r.Message) {
115+
if r.OK {
116+
continue
117+
}
118+
if IsSyntaxError(r.Message) {
86119
return Expectation{OK: false, Message: r.Message, Offset: r.ErrOffset}
87120
}
121+
// A semantic failure means the statement parsed -- but only as far
122+
// as the parser had got when the grammar action raised it.
123+
if r.Tail < r.End {
124+
unreached = append(unreached, Range{Start: r.Tail, End: r.End})
125+
}
88126
}
89-
return Expectation{OK: true, Offset: -1}
127+
return Expectation{OK: true, Offset: -1, Unreached: unreached}
90128
}
91129

92130
const (
@@ -132,6 +170,15 @@ func Read(path string) ([]Case, error) {
132170
c.Results = append(c.Results, r)
133171
i++
134172
}
173+
// Statement ends are implied by the next statement's offset, and by
174+
// the end of the case SQL for the last one.
175+
for j := range c.Results {
176+
if j+1 < len(c.Results) {
177+
c.Results[j].End = c.Results[j+1].Offset
178+
} else {
179+
c.Results[j].End = len(c.SQL)
180+
}
181+
}
135182
cases = append(cases, c)
136183
}
137184
return cases, nil
@@ -150,16 +197,19 @@ func parseResultLine(line string) (StmtResult, error) {
150197
if parts[1] == "ok" {
151198
return StmtResult{Offset: off, OK: true}, nil
152199
}
153-
fields := strings.SplitN(strings.TrimPrefix(parts[1], "err "), " ", 3)
154-
if !strings.HasPrefix(parts[1], "err ") || len(fields) != 3 {
200+
fields := strings.SplitN(strings.TrimPrefix(parts[1], "err "), " ", 4)
201+
if !strings.HasPrefix(parts[1], "err ") || len(fields) != 4 {
155202
return StmtResult{}, fmt.Errorf("malformed result line %q", line)
156203
}
157204
rc, err1 := strconv.Atoi(fields[0])
158205
erroff, err2 := strconv.Atoi(fields[1])
159-
if err1 != nil || err2 != nil {
206+
tail, err3 := strconv.Atoi(fields[2])
207+
if err1 != nil || err2 != nil || err3 != nil {
160208
return StmtResult{}, fmt.Errorf("malformed result line %q", line)
161209
}
162-
return StmtResult{Offset: off, RC: rc, ErrOffset: erroff, Message: fields[2]}, nil
210+
return StmtResult{
211+
Offset: off, RC: rc, ErrOffset: erroff, Tail: tail, Message: fields[3],
212+
}, nil
163213
}
164214

165215
// CheckCase reports whether a case can be represented in the file format
@@ -186,7 +236,8 @@ func Write(path string, cases []Case) error {
186236
if r.OK {
187237
fmt.Fprintf(&b, "stmt %d ok\n", r.Offset)
188238
} else {
189-
fmt.Fprintf(&b, "stmt %d err %d %d %s\n", r.Offset, r.RC, r.ErrOffset, r.Message)
239+
fmt.Fprintf(&b, "stmt %d err %d %d %d %s\n",
240+
r.Offset, r.RC, r.ErrOffset, r.Tail, r.Message)
190241
}
191242
}
192243
}

internal/testfile/testfile_test.go

Lines changed: 48 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,23 @@ import (
77
)
88

99
func TestRoundTrip(t *testing.T) {
10+
// Statement ends are derived on read from the following statement's
11+
// offset (and the length of the SQL for the last one), so they are
12+
// spelled out here even though Write does not emit them.
1013
cases := []Case{
1114
{
1215
Name: "a-1.1",
1316
SQL: "CREATE TABLE t1(a int);\nSELECT * FROM t1;\n",
1417
Results: []StmtResult{
15-
{Offset: 0, OK: true},
16-
{Offset: 24, RC: 1, ErrOffset: -1, Message: "no such table: t1"},
18+
{Offset: 0, End: 24, OK: true},
19+
{Offset: 24, End: 42, RC: 1, ErrOffset: -1, Tail: 42, Message: "no such table: t1"},
1720
},
1821
},
1922
{
2023
Name: "a-1.2",
2124
SQL: "SELEC 1;\n",
2225
Results: []StmtResult{
23-
{Offset: 0, RC: 1, ErrOffset: 0, Message: `near "SELEC": syntax error`},
26+
{Offset: 0, End: 9, RC: 1, ErrOffset: 0, Tail: 5, Message: `near "SELEC": syntax error`},
2427
},
2528
},
2629
}
@@ -39,23 +42,43 @@ func TestRoundTrip(t *testing.T) {
3942

4043
func TestExpected(t *testing.T) {
4144
tests := []struct {
42-
name string
43-
results []StmtResult
44-
wantOK bool
45-
wantMsg string
45+
name string
46+
results []StmtResult
47+
wantOK bool
48+
wantMsg string
49+
wantUnreached []Range
4650
}{
47-
{"all ok", []StmtResult{{OK: true}, {OK: true}}, true, ""},
48-
{"semantic only", []StmtResult{{RC: 1, ErrOffset: -1, Message: "no such table: t1"}}, true, ""},
51+
{"all ok", []StmtResult{{OK: true}, {OK: true}}, true, "", nil},
52+
{
53+
"semantic only",
54+
[]StmtResult{{End: 30, RC: 1, ErrOffset: -1, Tail: 30, Message: "no such table: t1"}},
55+
true, "", nil,
56+
},
4957
{
5058
"syntax after semantic",
5159
[]StmtResult{
52-
{Offset: 0, RC: 1, ErrOffset: -1, Message: "no such table: t1"},
53-
{Offset: 20, RC: 1, ErrOffset: 21, Message: `near "WHERE": syntax error`},
60+
{Offset: 0, End: 20, RC: 1, ErrOffset: -1, Tail: 20, Message: "no such table: t1"},
61+
{Offset: 20, End: 40, RC: 1, ErrOffset: 21, Tail: 21, Message: `near "WHERE": syntax error`},
5462
},
55-
false, `near "WHERE": syntax error`,
63+
false, `near "WHERE": syntax error`, nil,
64+
},
65+
{
66+
"unrecognized token",
67+
[]StmtResult{{End: 10, RC: 1, ErrOffset: 3, Tail: 3, Message: `unrecognized token: "0x"`}},
68+
false, `unrecognized token: "0x"`, nil,
69+
},
70+
{
71+
"incomplete",
72+
[]StmtResult{{End: 10, RC: 1, ErrOffset: -1, Tail: 10, Message: "incomplete input"}},
73+
false, "incomplete input", nil,
74+
},
75+
{
76+
// sqlite3BeginTrigger fails at the trigger_decl reduce, so
77+
// everything after BEGIN is never parsed.
78+
"semantic failure part-way through a statement",
79+
[]StmtResult{{Offset: 0, End: 62, RC: 1, ErrOffset: -1, Tail: 42, Message: "no such table: main.t1"}},
80+
true, "", []Range{{Start: 42, End: 62}},
5681
},
57-
{"unrecognized token", []StmtResult{{RC: 1, ErrOffset: 3, Message: `unrecognized token: "0x"`}}, false, `unrecognized token: "0x"`},
58-
{"incomplete", []StmtResult{{RC: 1, ErrOffset: -1, Message: "incomplete input"}}, false, "incomplete input"},
5982
}
6083
for _, tt := range tests {
6184
t.Run(tt.name, func(t *testing.T) {
@@ -64,6 +87,17 @@ func TestExpected(t *testing.T) {
6487
if exp.OK != tt.wantOK || exp.Message != tt.wantMsg {
6588
t.Fatalf("Expected() = %+v, want ok=%v msg=%q", exp, tt.wantOK, tt.wantMsg)
6689
}
90+
if !reflect.DeepEqual(exp.Unreached, tt.wantUnreached) {
91+
t.Fatalf("Unreached = %+v, want %+v", exp.Unreached, tt.wantUnreached)
92+
}
93+
for _, r := range tt.wantUnreached {
94+
if !exp.IsUnreached(r.Start) || exp.IsUnreached(r.End) {
95+
t.Fatalf("IsUnreached disagrees with %+v", r)
96+
}
97+
}
98+
if exp.IsUnreached(-1) {
99+
t.Fatal("IsUnreached(-1) should be false")
100+
}
67101
})
68102
}
69103
}

parser/parser_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,14 @@ func runCase(c testfile.Case) error {
8686
exp := c.Expected()
8787
if exp.OK {
8888
if err != nil {
89+
// A grammar action can fail part-way through a statement, and
90+
// SQLite then leaves the rest of it unparsed. The corpus records
91+
// how far it got; failing inside text SQLite never reached is
92+
// not a conformance failure, because nothing verified it.
93+
var pe *parser.Error
94+
if errors.As(err, &pe) && exp.IsUnreached(pe.Offset) {
95+
return nil
96+
}
8997
return fmt.Errorf("expected successful parse, got error: %v", err)
9098
}
9199
return nil

parser/testdata/README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,11 @@ The `*.test` files in this directory are generated by
1111
every case is prepared independently with `sqlite3_prepare_v2` against an
1212
empty in-memory database (nothing is executed) using the oracle program in
1313
`cmd/regenerate-parse/oracle.c`, compiled against the pinned amalgamation.
14-
The raw per-statement outcomes (OK, or result code + error offset + exact
15-
error message) are stored in the corpus files.
14+
The raw per-statement outcomes (OK, or result code + error offset +
15+
`pzTail` + exact error message) are stored in the corpus files. `pzTail`
16+
records how far SQLite's parser got: a grammar action can fail part-way
17+
through a statement and leave the rest of it unparsed, and the harness
18+
needs to know which text was never verified.
1619

1720
## Pinned SQLite release
1821

0 commit comments

Comments
 (0)