Skip to content

Commit 1491e3c

Browse files
jjtoltonclaude
andcommitted
Fix mthom#3172: Reject mismatched brackets like ([) and {([)}
Parser was incorrectly accepting mismatched bracket patterns: - ([) was returning '[' instead of syntax_error - {([)} was returning {[} instead of syntax_error Root cause: reduce_brackets() would find any Open/OpenCT on the stack and use it to close with ), even if there was an unclosed OpenList or OpenCurly between them. Fix: Add check in reduce_brackets() to reject if the last token on the stack is OpenList or OpenCurly when closing with ). Added comprehensive test suite with 22 test cases covering: - Mismatched bracket patterns (12 tests) - all should error - Valid nested bracket patterns (10 tests) - all should succeed ISO/IEC 13211-1:1995 Reference: - Each bracket type must close with its matching delimiter - ( with ), [ with ], { with } 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 39d12e6 commit 1491e3c

File tree

3 files changed

+301
-0
lines changed

3 files changed

+301
-0
lines changed

src/parser/parser.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -872,6 +872,17 @@ impl<'a, R: CharRead> Parser<'a, R> {
872872
return Ok(false);
873873
}
874874

875+
// Fix for issue #3172: Reject mismatched brackets
876+
// When closing with ), we must not have an unclosed [ or { on the stack
877+
// Example: ([) has stack [Open, OpenList] when ) arrives - this is invalid
878+
// ISO/IEC 13211-1:1995: Each bracket type must close with its matching closer
879+
if let Some(TokenType::OpenList | TokenType::OpenCurly) = self.stack.last().map(|token| token.tt) {
880+
return Err(ParserError::IncompleteReduction(
881+
self.lexer.line_num,
882+
self.lexer.col_num,
883+
));
884+
}
885+
875886
let idx = self.stack.len() - 2;
876887
let td = self.stack.remove(idx);
877888

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
Test 1 (([) should error): PASS
2+
Test 2 (({) should error): PASS
3+
Test 3 ({([)} should error): PASS
4+
Test 4 ([(]) should error): PASS
5+
Test 5 ({(}) should error): PASS
6+
Test 6 ({[} should error): PASS
7+
Test 7 ([{] should error): PASS
8+
Test 8 (((]) should error): PASS
9+
Test 9 ([) should error): PASS
10+
Test 10 ({) should error): PASS
11+
Test 11 ((] should error): PASS
12+
Test 12 ((} should error): PASS
13+
Test 13 (([]) valid): PASS
14+
Test 14 (({}) valid): PASS
15+
Test 15 ({[]} valid): PASS
16+
Test 17 ({([])} valid): PASS
17+
Test 18 ([{}] valid): PASS
18+
Test 19 (([a]) valid): PASS
19+
Test 20 (({a}) valid): PASS
20+
Test 21 ({[a,b]} valid): PASS
21+
Test 22 ([a,[b],c] valid): PASS
22+
Test 23 ({a,{b},c} valid): PASS
Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
%% Test file for issue #3172: Mismatched bracket detection
2+
%%
3+
%% GitHub: https://github.com/mthom/scryer-prolog/pull/3172#issuecomment-3574685968
4+
%%
5+
%% Bug Report:
6+
%% - read_from_chars("([).", T) returns T = '[' instead of syntax_error
7+
%% - read_from_chars("{([)}.", T) returns T = {[} instead of syntax_error
8+
%%
9+
%% ISO/IEC 13211-1:1995 References:
10+
%% - Section 6.3: Brackets must be properly matched
11+
%% - ( must close with )
12+
%% - [ must close with ]
13+
%% - { must close with }
14+
%% - Section 6.3.4: List notation - [items] denotes list
15+
%% - Section 6.3.6: Curly bracketed term - {term} == '{}'(term)
16+
%%
17+
%% Mismatched brackets should always result in syntax_error.
18+
19+
:- use_module(library(charsio)).
20+
21+
%% ============================================================================
22+
%% TESTS FOR MISMATCHED BRACKETS (should all fail with syntax_error)
23+
%% ============================================================================
24+
25+
%% Test 1: ([) - paren containing unclosed list, closed with paren
26+
%% Expected: syntax_error (mismatched: [ not closed with ])
27+
test1 :-
28+
catch(
29+
(read_from_chars("([).", _), fail),
30+
error(syntax_error(_), _),
31+
true
32+
).
33+
34+
%% Test 2: ({) - paren containing unclosed curly, closed with paren
35+
%% Expected: syntax_error (mismatched: { not closed with })
36+
test2 :-
37+
catch(
38+
(read_from_chars("({).", _), fail),
39+
error(syntax_error(_), _),
40+
true
41+
).
42+
43+
%% Test 3: {([)} - curly containing mismatched inner brackets
44+
%% Expected: syntax_error (mismatched: [ not closed with ])
45+
test3 :-
46+
catch(
47+
(read_from_chars("{([)}.", _), fail),
48+
error(syntax_error(_), _),
49+
true
50+
).
51+
52+
%% Test 4: [(]) - list containing mismatched parens
53+
%% Expected: syntax_error (mismatched: ( not closed with ))
54+
test4 :-
55+
catch(
56+
(read_from_chars("[(]).", _), fail),
57+
error(syntax_error(_), _),
58+
true
59+
).
60+
61+
%% Test 5: {(}) - curly containing mismatched parens
62+
%% Expected: syntax_error (mismatched: ( not closed with ))
63+
test5 :-
64+
catch(
65+
(read_from_chars("{(}).", _), fail),
66+
error(syntax_error(_), _),
67+
true
68+
).
69+
70+
%% Test 6: {[} - curly containing unclosed list
71+
%% Expected: syntax_error (mismatched: [ not closed with ])
72+
test6 :-
73+
catch(
74+
(read_from_chars("{[}.", _), fail),
75+
error(syntax_error(_), _),
76+
true
77+
).
78+
79+
%% Test 7: [{] - list containing unclosed curly
80+
%% Expected: syntax_error (mismatched: { not closed with })
81+
test7 :-
82+
catch(
83+
(read_from_chars("[{].", _), fail),
84+
error(syntax_error(_), _),
85+
true
86+
).
87+
88+
%% Test 8: ((]) - nested parens with mismatched list closer
89+
%% Expected: syntax_error
90+
test8 :-
91+
catch(
92+
(read_from_chars("((]).", _), fail),
93+
error(syntax_error(_), _),
94+
true
95+
).
96+
97+
%% Test 9: [) - bare list closed with paren
98+
%% Expected: syntax_error
99+
test9 :-
100+
catch(
101+
(read_from_chars("[).", _), fail),
102+
error(syntax_error(_), _),
103+
true
104+
).
105+
106+
%% Test 10: {) - bare curly closed with paren
107+
%% Expected: syntax_error
108+
test10 :-
109+
catch(
110+
(read_from_chars("{).", _), fail),
111+
error(syntax_error(_), _),
112+
true
113+
).
114+
115+
%% Test 11: (] - bare paren closed with list closer
116+
%% Expected: syntax_error
117+
test11 :-
118+
catch(
119+
(read_from_chars("(].", _), fail),
120+
error(syntax_error(_), _),
121+
true
122+
).
123+
124+
%% Test 12: (} - bare paren closed with curly closer
125+
%% Expected: syntax_error
126+
test12 :-
127+
catch(
128+
(read_from_chars("(}.", _), fail),
129+
error(syntax_error(_), _),
130+
true
131+
).
132+
133+
%% ============================================================================
134+
%% TESTS FOR VALID NESTED BRACKETS (should all succeed)
135+
%% ============================================================================
136+
137+
%% Test 13: ([]) - paren containing empty list
138+
%% Expected: T = []
139+
test13 :-
140+
read_from_chars("([]).", T),
141+
T = [].
142+
143+
%% Test 14: ({}) - paren containing empty curly
144+
%% Expected: T = {}
145+
test14 :-
146+
read_from_chars("({}).", T),
147+
T = {}.
148+
149+
%% Test 15: {[]} - curly containing empty list
150+
%% Expected: T = {[]}
151+
test15 :-
152+
read_from_chars("{[]}.", T),
153+
T = '{}'([]).
154+
155+
%% Test 17: {([])} - curly containing paren containing list
156+
%% Expected: T = {[]}
157+
test17 :-
158+
read_from_chars("{([])}.", T),
159+
T = '{}'([]).
160+
161+
%% Test 18: [{}] - list containing empty curly
162+
%% Expected: T = [{}]
163+
test18 :-
164+
read_from_chars("[{}].", T),
165+
T = [{}].
166+
167+
%% Test 19: ([a]) - paren containing list with element
168+
%% Expected: T = [a]
169+
test19 :-
170+
read_from_chars("([a]).", T),
171+
T = [a].
172+
173+
%% Test 20: ({a}) - paren containing curly with element
174+
%% Expected: T = {a}
175+
test20 :-
176+
read_from_chars("({a}).", T),
177+
T = '{}'(a).
178+
179+
%% Test 21: {[a,b]} - curly containing list with elements
180+
%% Expected: T = {[a,b]}
181+
test21 :-
182+
read_from_chars("{[a,b]}.", T),
183+
T = '{}'([a,b]).
184+
185+
%% Test 22: [a,[b],c] - list containing nested list
186+
%% Expected: T = [a,[b],c]
187+
test22 :-
188+
read_from_chars("[a,[b],c].", T),
189+
T = [a,[b],c].
190+
191+
%% Test 23: {a,{b},c} - curly containing nested curly
192+
%% Expected: T = {','(a,','({b},c))}
193+
test23 :-
194+
read_from_chars("{a,{b},c}.", T),
195+
T = '{}'(','(a,','('{}'(b),c))).
196+
197+
%% ============================================================================
198+
%% MAIN TEST RUNNER
199+
%% ============================================================================
200+
201+
run :-
202+
write('Test 1 (([) should error): '),
203+
(test1 -> write('PASS') ; write('FAIL')), nl,
204+
205+
write('Test 2 (({) should error): '),
206+
(test2 -> write('PASS') ; write('FAIL')), nl,
207+
208+
write('Test 3 ({([)} should error): '),
209+
(test3 -> write('PASS') ; write('FAIL')), nl,
210+
211+
write('Test 4 ([(]) should error): '),
212+
(test4 -> write('PASS') ; write('FAIL')), nl,
213+
214+
write('Test 5 ({(}) should error): '),
215+
(test5 -> write('PASS') ; write('FAIL')), nl,
216+
217+
write('Test 6 ({[} should error): '),
218+
(test6 -> write('PASS') ; write('FAIL')), nl,
219+
220+
write('Test 7 ([{] should error): '),
221+
(test7 -> write('PASS') ; write('FAIL')), nl,
222+
223+
write('Test 8 (((]) should error): '),
224+
(test8 -> write('PASS') ; write('FAIL')), nl,
225+
226+
write('Test 9 ([) should error): '),
227+
(test9 -> write('PASS') ; write('FAIL')), nl,
228+
229+
write('Test 10 ({) should error): '),
230+
(test10 -> write('PASS') ; write('FAIL')), nl,
231+
232+
write('Test 11 ((] should error): '),
233+
(test11 -> write('PASS') ; write('FAIL')), nl,
234+
235+
write('Test 12 ((} should error): '),
236+
(test12 -> write('PASS') ; write('FAIL')), nl,
237+
238+
write('Test 13 (([]) valid): '),
239+
(test13 -> write('PASS') ; write('FAIL')), nl,
240+
241+
write('Test 14 (({}) valid): '),
242+
(test14 -> write('PASS') ; write('FAIL')), nl,
243+
244+
write('Test 15 ({[]} valid): '),
245+
(test15 -> write('PASS') ; write('FAIL')), nl,
246+
247+
write('Test 17 ({([])} valid): '),
248+
(test17 -> write('PASS') ; write('FAIL')), nl,
249+
250+
write('Test 18 ([{}] valid): '),
251+
(test18 -> write('PASS') ; write('FAIL')), nl,
252+
253+
write('Test 19 (([a]) valid): '),
254+
(test19 -> write('PASS') ; write('FAIL')), nl,
255+
256+
write('Test 20 (({a}) valid): '),
257+
(test20 -> write('PASS') ; write('FAIL')), nl,
258+
259+
write('Test 21 ({[a,b]} valid): '),
260+
(test21 -> write('PASS') ; write('FAIL')), nl,
261+
262+
write('Test 22 ([a,[b],c] valid): '),
263+
(test22 -> write('PASS') ; write('FAIL')), nl,
264+
265+
write('Test 23 ({a,{b},c} valid): '),
266+
(test23 -> write('PASS') ; write('FAIL')), nl.
267+
268+
:- initialization(run).

0 commit comments

Comments
 (0)