Bracket checker
Checks the round, square and curly brackets of a typed text of up to 20 lines: the first error with its line, column and reason - a bracket that closes nothing, one of the wrong kind, or one never closed - the deepest nesting, and an outline of the text broken at its brackets.
Every screen below was recorded under CPython. When this page was built, the EML interpreter replayed each session from the same input and printed the same bytes.
About
Checks the round, square and curly brackets of a typed text of up to 20 lines. It finds the first error and says where it is and why: a closing bracket with nothing to close, a closing bracket of the wrong kind, or brackets still open when the text ends. For a text whose brackets all match, it gives the number of pairs of each kind and the deepest nesting, and it can outline the text by its brackets.
main.eml- the menu, reading the lines of a text, and what the screen showsbrackets.eml- the scan for the first error and the deepest nesting, and the outlinetext.eml- trimming, spaces, lists in words and plurals
How each part works:
- The scan: every opening bracket goes on a stack with its line and column. A closing bracket must match the bracket on top of the stack, which it removes. The first one that does not - because the stack is empty, or because the top is another kind - is the error; the line is shown with a caret under the column. Brackets still on the stack at the end were never closed: they are listed outermost first (five at most, then "and N more"), with the closing brackets they need, innermost first.
- The deepest nesting is the stack's greatest height, with the line and column of the bracket that first reached it.
- The outline joins the lines with spaces and breaks the text after every opening bracket and before every closing one, indenting each piece two spaces per level. An empty pair such as
()or[]stays where it is.
What is checked: a text is 1 to 20 lines, ended by an empty line (or a line of spaces); a line longer than 120 characters is asked for again; at 20 lines the text ends by itself. The outline needs every bracket matched. A menu choice other than 1, 2 or 3 is asked again.
Sessions: sessions/basic.in checks a three-line if block (6 pairs, deepest nesting 3), outlines a JSON object with nested lists and objects, finds print(a[1)] closing a [ with ), finds a ( and a [ left open at the end of a function, and outlines a while loop; sessions/bad-input.in types menu choices that are not 1 to 3, empty texts, a ) with nothing to close, text with no brackets to check or to outline, a mismatch where an outline was asked for, seven nested pairs, a line of 121 characters, six brackets never closed, and 20 lines.
Built on the verified corpus cases balanced-brackets-checker (a list used as a stack: push on an opener, compare and pop on a closer) and bracket-tree-outline (a bracketed text shown as an indented outline).
Recorded sessions
What the screen shows while someone uses the program. Each typed line appears after its prompt, the way a terminal shows it.
bad-input
interpreter: byte-equal
== Bracket checker ==
1) check a text 2) outline a text 3) quit
choice> 7
Pick 1, 2 or 3.
== Bracket checker ==
1) check a text 2) outline a text 3) quit
choice> hello
Pick 1, 2 or 3.
== Bracket checker ==
1) check a text 2) outline a text 3) quit
choice> 1
Type the text one line at a time (at most 20 lines); an empty line ends it.
>
No text.
== Bracket checker ==
1) check a text 2) outline a text 3) quit
choice> 2
Type the text one line at a time (at most 20 lines); an empty line ends it.
>
No text.
== Bracket checker ==
1) check a text 2) outline a text 3) quit
choice> 1
Type the text one line at a time (at most 20 lines); an empty line ends it.
> a = b)
>
Line 1, column 6: ')' has nothing to close.
1 | a = b)
| ^
== Bracket checker ==
1) check a text 2) outline a text 3) quit
choice> 1
Type the text one line at a time (at most 20 lines); an empty line ends it.
> just words, no brackets
> here either
>
No brackets in 2 lines.
== Bracket checker ==
1) check a text 2) outline a text 3) quit
choice> 2
Type the text one line at a time (at most 20 lines); an empty line ends it.
> plain text
>
No brackets to outline.
== Bracket checker ==
1) check a text 2) outline a text 3) quit
choice> 2
Type the text one line at a time (at most 20 lines); an empty line ends it.
> ((a + b) * [c - d}
>
Line 1, column 18: '}' does not match the '[' opened at line 1, column 12.
1 | ((a + b) * [c - d}
| ^
The outline needs every bracket matched.
== Bracket checker ==
1) check a text 2) outline a text 3) quit
choice> 2
Type the text one line at a time (at most 20 lines); an empty line ends it.
> (((((((x)))))))
>
(
(
(
(
(
(
(
x
)
)
)
)
)
)
)
== Bracket checker ==
1) check a text 2) outline a text 3) quit
choice> 1
Type the text one line at a time (at most 20 lines); an empty line ends it.
> (aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa)
That line has 121 characters; a line can have at most 120. Type it again.
> short (line)
>
1 line, 1 pair of brackets: 1 ( ).
Deepest nesting: 1, first reached at line 1, column 7.
Every bracket is closed, in the right order.
== Bracket checker ==
1) check a text 2) outline a text 3) quit
choice> 1
Type the text one line at a time (at most 20 lines); an empty line ends it.
> {a
> [b
> (c
> {d
> [e
> (f
>
End of text: 6 brackets are never closed - '{' at line 1, column 1, '[' at line 2, column 1, '(' at line 3, column 1, '{' at line 4, column 1, '[' at line 5, column 1 and 1 more.
Add )]})]} to close them, innermost first.
== Bracket checker ==
1) check a text 2) outline a text 3) quit
choice> 1
Type the text one line at a time (at most 20 lines); an empty line ends it.
> x1 = (1)
> x2 = (2)
> x3 = (3)
> x4 = (4)
> x5 = (5)
> x6 = (6)
> x7 = (7)
> x8 = (8)
> x9 = (9)
> x10 = (10)
> x11 = (11)
> x12 = (12)
> x13 = (13)
> x14 = (14)
> x15 = (15)
> x16 = (16)
> x17 = (17)
> x18 = (18)
> x19 = (19)
> x20 = (20)
That is 20 lines, the most a text can have.
20 lines, 20 pairs of brackets: 20 ( ).
Deepest nesting: 1, first reached at line 1, column 6.
Every bracket is closed, in the right order.
== Bracket checker ==
1) check a text 2) outline a text 3) quit
choice> 2
Type the text one line at a time (at most 20 lines); an empty line ends it.
>
No text.
== Bracket checker ==
1) check a text 2) outline a text 3) quit
choice> 3
Bye.
What was typed (58 lines)
7
hello
1
2
1
a = b)
1
just words, no brackets
here either
2
plain text
2
((a + b) * [c - d}
2
(((((((x)))))))
1
(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa)
short (line)
1
{a
[b
(c
{d
[e
(f
1
x1 = (1)
x2 = (2)
x3 = (3)
x4 = (4)
x5 = (5)
x6 = (6)
x7 = (7)
x8 = (8)
x9 = (9)
x10 = (10)
x11 = (11)
x12 = (12)
x13 = (13)
x14 = (14)
x15 = (15)
x16 = (16)
x17 = (17)
x18 = (18)
x19 = (19)
x20 = (20)
2
3
basic
interpreter: byte-equal
== Bracket checker ==
1) check a text 2) outline a text 3) quit
choice> 1
Type the text one line at a time (at most 20 lines); an empty line ends it.
> if (total > limit[0]) {
> report(items[i], {name: "x"})
> }
>
3 lines, 6 pairs of brackets: 2 ( ), 2 [ ] and 2 { }.
Deepest nesting: 3, first reached at line 2, column 15.
Every bracket is closed, in the right order.
== Bracket checker ==
1) check a text 2) outline a text 3) quit
choice> 2
Type the text one line at a time (at most 20 lines); an empty line ends it.
> {"name": "Ana", "tags": ["a", "b"], "box": {"w": 2, "h": [1, 2]}, "none": []}
>
{
"name": "Ana", "tags": [
"a", "b"
], "box": {
"w": 2, "h": [
1, 2
]
}, "none": []
}
== Bracket checker ==
1) check a text 2) outline a text 3) quit
choice> 1
Type the text one line at a time (at most 20 lines); an empty line ends it.
> print(a[1)]
>
Line 1, column 10: ')' does not match the '[' opened at line 1, column 8.
1 | print(a[1)]
| ^
== Bracket checker ==
1) check a text 2) outline a text 3) quit
choice> 1
Type the text one line at a time (at most 20 lines); an empty line ends it.
> def f(x):
> return g(x, [1, 2
>
End of text: 2 brackets are never closed - '(' at line 2, column 11 and '[' at line 2, column 15.
Add ]) to close them, innermost first.
== Bracket checker ==
1) check a text 2) outline a text 3) quit
choice> 2
Type the text one line at a time (at most 20 lines); an empty line ends it.
> while (n > 0) { total = total + f(n, [n - 1]); n = n - 1 }
>
while (
n > 0
) {
total = total + f(
n, [
n - 1
]
); n = n - 1
}
== Bracket checker ==
1) check a text 2) outline a text 3) quit
choice> 3
Bye.
What was typed (19 lines)
1
if (total > limit[0]) {
report(items[i], {name: "x"})
}
2
{"name": "Ana", "tags": ["a", "b"], "box": {"w": 2, "h": [1, 2]}, "none": []}
1
print(a[1)]
1
def f(x):
return g(x, [1, 2
2
while (n > 0) { total = total + f(n, [n - 1]); n = n - 1 }
3
Modules
The program as written, entry module first. Each module transpiles to its own Python file, which is what eml project run executes.
main.eml(entry)
eml# P024 bracket checker: the first bracket error in a text, with its line and
# column, the deepest nesting, and an outline of the text by its brackets.
import brackets
import text
20 => most_lines
120 => widest_line
def read_text():
# Lines until an empty one: at most most_lines, each at most widest_line
# characters. Columns count from the start of a line as typed.
("Type the text one line at a time (at most " + str(most_lines) + " lines); an empty line ends it.") ^0
[] => lines
while len(lines) < most_lines:
input("> ") => line
if text.trim(line) == "":
return lines
if len(line) > widest_line:
("That line has " + str(len(line)) + " characters; a line can have at most " + str(widest_line) + ". Type it again.") ^0
else:
lines + [line] => lines
("That is " + str(most_lines) + " lines, the most a text can have.") ^0
return lines
def where(line_no, col):
return "line " + str(line_no) + ", column " + str(col)
def show_at(lines, line_no, col):
# The line, numbered, with a caret under the column.
str(line_no) => num
(" " + num + " | " + lines[line_no - 1]) ^0
(" " + text.spaces(len(num)) + " | " + text.spaces(col - 1) + "^") ^0
def pair_kinds(pairs):
[] => parts
["( )", "[ ]", "{ }"] => shapes
for k in [0:2]:
if pairs[k] > 0:
parts + [str(pairs[k]) + " " + shapes[k]] => parts
return text.listed(parts)
def report(lines, r):
# Shows a scan result; True when every bracket matched.
if r[0] == "stray":
("Line " + str(r[1]) + ", column " + str(r[2]) + ": '" + r[3] + "' has nothing to close.") ^0
show_at(lines, r[1], r[2])
return False
if r[0] == "mismatch":
("Line " + str(r[1]) + ", column " + str(r[2]) + ": '" + r[3] + "' does not match the '" + r[4] + "' opened at " + where(r[5], r[6]) + ".") ^0
show_at(lines, r[1], r[2])
return False
if r[0] == "unclosed":
r[1] => still_open
if len(still_open) == 1:
("End of text: the '" + still_open[0][0] + "' opened at " + where(still_open[0][1], still_open[0][2]) + " is never closed.") ^0
("Add " + brackets.still_to_close(still_open) + " to close it.") ^0
else:
[] => named
for o in still_open:
if len(named) < 5:
named + ["'" + o[0] + "' at " + where(o[1], o[2])] => named
if len(still_open) > 5:
named + [str(len(still_open) - 5) + " more"] => named
("End of text: " + str(len(still_open)) + " brackets are never closed - " + text.listed(named) + ".") ^0
("Add " + brackets.still_to_close(still_open) + " to close them, innermost first.") ^0
return False
r[1] => pairs
pairs[0] + pairs[1] + pairs[2] => total
if total == 0:
("No brackets in " + text.plural(len(lines), "line") + ".") ^0
else:
(text.plural(len(lines), "line") + ", " + text.plural(total, "pair") + " of brackets: " + pair_kinds(pairs) + ".") ^0
("Deepest nesting: " + str(r[2]) + ", first reached at " + where(r[3], r[4]) + ".") ^0
"Every bracket is closed, in the right order." ^0
return True
True => running
while running:
"" ^0
"== Bracket checker ==" ^0
"1) check a text 2) outline a text 3) quit" ^0
text.trim(input("choice> ")) => choice
if choice == "1":
read_text() => lines
if len(lines) == 0:
"No text." ^0
else:
report(lines, brackets.scan(lines)) => ok
elif choice == "2":
read_text() => lines
if len(lines) == 0:
"No text." ^0
else:
brackets.scan(lines) => r
if r[0] != "ok":
report(lines, r) => ok
"The outline needs every bracket matched." ^0
elif r[1][0] + r[1][1] + r[1][2] == 0:
"No brackets to outline." ^0
else:
for line in brackets.outline(lines):
line ^0
elif choice == "3":
False => running
else:
"Pick 1, 2 or 3." ^0
"Bye." ^0
Python projection (main.py)
import brackets
import text
most_lines = 20
widest_line = 120
def read_text():
print("Type the text one line at a time (at most " + str(most_lines) + " lines); an empty line ends it.")
lines = []
while len(lines) < most_lines:
line = input("> ")
if text.trim(line) == "":
return lines
if len(line) > widest_line:
print("That line has " + str(len(line)) + " characters; a line can have at most " + str(widest_line) + ". Type it again.")
else:
lines = lines + [line]
print("That is " + str(most_lines) + " lines, the most a text can have.")
return lines
def where(line_no, col):
return "line " + str(line_no) + ", column " + str(col)
def show_at(lines, line_no, col):
num = str(line_no)
print(" " + num + " | " + lines[line_no - 1])
print(" " + text.spaces(len(num)) + " | " + text.spaces(col - 1) + "^")
def pair_kinds(pairs):
parts = []
shapes = ["( )", "[ ]", "{ }"]
for k in range(0, 3):
if pairs[k] > 0:
parts = parts + [str(pairs[k]) + " " + shapes[k]]
return text.listed(parts)
def report(lines, r):
if r[0] == "stray":
print("Line " + str(r[1]) + ", column " + str(r[2]) + ": '" + r[3] + "' has nothing to close.")
show_at(lines, r[1], r[2])
return False
if r[0] == "mismatch":
print("Line " + str(r[1]) + ", column " + str(r[2]) + ": '" + r[3] + "' does not match the '" + r[4] + "' opened at " + where(r[5], r[6]) + ".")
show_at(lines, r[1], r[2])
return False
if r[0] == "unclosed":
still_open = r[1]
if len(still_open) == 1:
print("End of text: the '" + still_open[0][0] + "' opened at " + where(still_open[0][1], still_open[0][2]) + " is never closed.")
print("Add " + brackets.still_to_close(still_open) + " to close it.")
else:
named = []
for o in still_open:
if len(named) < 5:
named = named + ["'" + o[0] + "' at " + where(o[1], o[2])]
if len(still_open) > 5:
named = named + [str(len(still_open) - 5) + " more"]
print("End of text: " + str(len(still_open)) + " brackets are never closed - " + text.listed(named) + ".")
print("Add " + brackets.still_to_close(still_open) + " to close them, innermost first.")
return False
pairs = r[1]
total = pairs[0] + pairs[1] + pairs[2]
if total == 0:
print("No brackets in " + text.plural(len(lines), "line") + ".")
else:
print(text.plural(len(lines), "line") + ", " + text.plural(total, "pair") + " of brackets: " + pair_kinds(pairs) + ".")
print("Deepest nesting: " + str(r[2]) + ", first reached at " + where(r[3], r[4]) + ".")
print("Every bracket is closed, in the right order.")
return True
running = True
while running:
print("")
print("== Bracket checker ==")
print("1) check a text 2) outline a text 3) quit")
choice = text.trim(input("choice> "))
if choice == "1":
lines = read_text()
if len(lines) == 0:
print("No text.")
else:
ok = report(lines, brackets.scan(lines))
elif choice == "2":
lines = read_text()
if len(lines) == 0:
print("No text.")
else:
r = brackets.scan(lines)
if r[0] != "ok":
ok = report(lines, r)
print("The outline needs every bracket matched.")
elif r[1][0] + r[1][1] + r[1][2] == 0:
print("No brackets to outline.")
else:
for line in brackets.outline(lines):
print(line)
elif choice == "3":
running = False
else:
print("Pick 1, 2 or 3.")
print("Bye.")
brackets.eml
eml# P024 bracket checker - walking the brackets of a text: the first error, the
# deepest nesting, and an outline that breaks the text at its brackets.
def closer_for(c):
# The bracket that closes c, or "" if c does not open one.
if c == "(":
return ")"
if c == "[":
return "]"
if c == "{":
return "}"
return ""
def opener_for(c):
# The bracket that c closes, or "" if c does not close one.
if c == ")":
return "("
if c == "]":
return "["
if c == "}":
return "{"
return ""
def kind_index(c):
# 0 for round, 1 for square, 2 for curly; c is an opening bracket.
if c == "(":
return 0
if c == "[":
return 1
return 2
def scan(lines):
# Every bracket in order, kept on a stack of [bracket, line, column]. The
# result is one of:
# ["ok", pairs, deepest, line, column] - every bracket matched; pairs
# counts round, square and curly pairs, and the deepest nesting is
# first reached at line, column (both 0 when there are no brackets)
# ["stray", line, column, closer] - a closing bracket with nothing open
# ["mismatch", line, column, closer, opener, its line, its column]
# ["unclosed", stack] - the text ends with these still open, outermost
# first
[] => stack
[0, 0, 0] => pairs
0 => deepest
0 => deep_line
0 => deep_col
0 => li
while li < len(lines):
lines[li] => line
0 => ci
while ci < len(line):
line[ci] => c
if closer_for(c) != "":
stack + [[c, li + 1, ci + 1]] => stack
if len(stack) > deepest:
len(stack) => deepest
li + 1 => deep_line
ci + 1 => deep_col
elif opener_for(c) != "":
if len(stack) == 0:
return ["stray", li + 1, ci + 1, c]
stack[len(stack) - 1] => top
if top[0] != opener_for(c):
return ["mismatch", li + 1, ci + 1, c, top[0], top[1], top[2]]
pairs[kind_index(top[0])] + 1 => pairs[kind_index(top[0])]
stack[0:len(stack) - 1] => stack
ci + 1 => ci
li + 1 => li
if len(stack) > 0:
return ["unclosed", stack]
return ["ok", pairs, deepest, deep_line, deep_col]
def still_to_close(stack):
# The closing brackets the open ones need, innermost first: "])}".
"" => out
len(stack) - 1 => i
while i >= 0:
out + closer_for(stack[i][0]) => out
i - 1 => i
return out
def trimmed(s):
0 => i
len(s) => j
while i < j and s[i] == " ":
i + 1 => i
while j > i and s[j - 1] == " ":
j - 1 => j
return s[i:j]
def next_solid(s, i):
# The first position from i on that is not a space (len(s) if none).
while i < len(s) and s[i] == " ":
i + 1 => i
return i
def outline(lines):
# The text, its lines joined by spaces, broken after every opening bracket
# and before every closing one, each piece indented two spaces per level.
# An empty pair such as () stays where it is. Only for text whose brackets
# all match.
"" => s
for line in lines:
if s != "":
s + " " => s
s + line => s
[] => out
"" => piece
0 => depth
0 => i
while i < len(s):
s[i] => c
if closer_for(c) != "":
next_solid(s, i + 1) => j
if j < len(s) and opener_for(s[j]) != "":
piece + s[i:j + 1] => piece
j + 1 => i
else:
out + [" " * depth + trimmed(piece + c)] => out
"" => piece
depth + 1 => depth
i + 1 => i
elif opener_for(c) != "":
if trimmed(piece) != "":
out + [" " * depth + trimmed(piece)] => out
depth - 1 => depth
c => piece
i + 1 => i
else:
if not (c == " " and piece == ""):
piece + c => piece
i + 1 => i
if trimmed(piece) != "":
out + [" " * depth + trimmed(piece)] => out
return out
Python projection (brackets.py)
def closer_for(c):
if c == "(":
return ")"
if c == "[":
return "]"
if c == "{":
return "}"
return ""
def opener_for(c):
if c == ")":
return "("
if c == "]":
return "["
if c == "}":
return "{"
return ""
def kind_index(c):
if c == "(":
return 0
if c == "[":
return 1
return 2
def scan(lines):
stack = []
pairs = [0, 0, 0]
deepest = 0
deep_line = 0
deep_col = 0
li = 0
while li < len(lines):
line = lines[li]
ci = 0
while ci < len(line):
c = line[ci]
if closer_for(c) != "":
stack = stack + [[c, li + 1, ci + 1]]
if len(stack) > deepest:
deepest = len(stack)
deep_line = li + 1
deep_col = ci + 1
elif opener_for(c) != "":
if len(stack) == 0:
return ["stray", li + 1, ci + 1, c]
top = stack[len(stack) - 1]
if top[0] != opener_for(c):
return ["mismatch", li + 1, ci + 1, c, top[0], top[1], top[2]]
pairs[kind_index(top[0])] = pairs[kind_index(top[0])] + 1
stack = stack[0:len(stack) - 1]
ci = ci + 1
li = li + 1
if len(stack) > 0:
return ["unclosed", stack]
return ["ok", pairs, deepest, deep_line, deep_col]
def still_to_close(stack):
out = ""
i = len(stack) - 1
while i >= 0:
out = out + closer_for(stack[i][0])
i = i - 1
return out
def trimmed(s):
i = 0
j = len(s)
while i < j and s[i] == " ":
i = i + 1
while j > i and s[j - 1] == " ":
j = j - 1
return s[i:j]
def next_solid(s, i):
while i < len(s) and s[i] == " ":
i = i + 1
return i
def outline(lines):
s = ""
for line in lines:
if s != "":
s = s + " "
s = s + line
out = []
piece = ""
depth = 0
i = 0
while i < len(s):
c = s[i]
if closer_for(c) != "":
j = next_solid(s, i + 1)
if j < len(s) and opener_for(s[j]) != "":
piece = piece + s[i:j + 1]
i = j + 1
else:
out = out + [" " * depth + trimmed(piece + c)]
piece = ""
depth = depth + 1
i = i + 1
elif opener_for(c) != "":
if trimmed(piece) != "":
out = out + [" " * depth + trimmed(piece)]
depth = depth - 1
piece = c
i = i + 1
else:
if not (c == " " and piece == ""):
piece = piece + c
i = i + 1
if trimmed(piece) != "":
out = out + [" " * depth + trimmed(piece)]
return out
text.eml
eml# P024 bracket checker - small text helpers. The interpreter that checks every
# session does not run string methods yet, so these are written out here.
def trim(s):
# s without the spaces at either end.
0 => i
len(s) => j
while i < j and s[i] == " ":
i + 1 => i
while j > i and s[j - 1] == " ":
j - 1 => j
return s[i:j]
def spaces(n):
return " " * n
def listed(items):
# ["a", "b", "c"] as "a, b and c".
"" => out
0 => i
while i < len(items):
if i > 0 and i == len(items) - 1:
out + " and " => out
elif i > 0:
out + ", " => out
out + items[i] => out
i + 1 => i
return out
def plural(n, word):
if n == 1:
return "1 " + word
return str(n) + " " + word + "s"
Python projection (text.py)
def trim(s):
i = 0
j = len(s)
while i < j and s[i] == " ":
i = i + 1
while j > i and s[j - 1] == " ":
j = j - 1
return s[i:j]
def spaces(n):
return " " * n
def listed(items):
out = ""
i = 0
while i < len(items):
if i > 0 and i == len(items) - 1:
out = out + " and "
elif i > 0:
out = out + ", "
out = out + items[i]
i = i + 1
return out
def plural(n, word):
if n == 1:
return "1 " + word
return str(n) + " " + word + "s"