1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
|
local lpeg = require"lpeglabel"
local R, S, P, V = lpeg.R, lpeg.S, lpeg.P, lpeg.V
local C, Cc, Ct, Cmt = lpeg.C, lpeg.Cc, lpeg.Ct, lpeg.Cmt
local T, Lc = lpeg.T, lpeg.Lc
-- The `labels` table contains the list of labels that we will be using
-- as well as the corresponding error message for each label, which will
-- be used in our error reporting later on.
local labels = {
{"NoExp", "no expression found"},
{"Extra", "extra characters found after the expression"},
{"ExpTerm", "expected a term after the operator"},
{"ExpExp", "expected an expression after the parenthesis"},
{"MisClose", "missing a closing ')' after the expression"},
}
-- The `labelindex` function gives us the index of a label in the
-- `labels` table, which serves as the integer representation of the label.
-- We need this because LPegLabel requires us to use integers for the labels.
local function labelindex(labname)
for i, elem in ipairs(labels) do
if elem[1] == labname then
return i
end
end
error("could not find label: " .. labname)
end
-- The `errors` table will hold the list of errors recorded during parsing
local errors = {}
-- The `expect` function takes a pattern and a label and returns a pattern
-- that throws the specified label if the original pattern fails to match.
-- Before throwing the label, it records the label to be thrown along with
-- the position of the failure (index in input string) into the `errors` table.
local function expect(patt, labname)
local i = labelindex(labname)
function recorderror(input, pos)
table.insert(errors, {i, pos})
return true
end
return patt + Cmt("", recorderror) * T(i)
end
local num = R("09")^1 / tonumber
local op = S("+-*/")
-- The `compute` function takes an alternating list of numbers and
-- operators and computes the result of applying the operations
-- to the numbers in a left to right order (no operator precedence).
local function compute(tokens)
local result = tokens[1]
for i = 2, #tokens, 2 do
if tokens[i] == '+' then
result = result + tokens[i+1]
elseif tokens[i] == '-' then
result = result - tokens[i+1]
elseif tokens[i] == '*' then
result = result * tokens[i+1]
elseif tokens[i] == '/' then
result = result / tokens[i+1]
else
error('unknown operation: ' .. tokens[i])
end
end
return result
end
-- Our grammar is a simple arithmetic expression of integers that
-- does not take operator precedence into account but allows grouping
-- via parenthesis. We have incorporated some error recovery startegies
-- to our grammar so that it may resume parsing even after encountering
-- an error, which allows us to report more errors.
local g = P {
"Exp",
Exp = Ct(V"Term" * (C(op) * V"OpRecov")^0) / compute;
-- `OpRecov` handles missing terms/operands by returning a dummy (zero).
OpRecov = Lc(V"Operand", Cc(0), labelindex("ExpTerm"));
Operand = expect(V"Term", "ExpTerm");
Term = num + V"Group";
-- `Group` handles missing closing parenthesis by simply ignoring it.
-- Like all the others, the error is still recorded of course.
Group = "(" * V"InnerExp" * Lc(expect(")", "MisClose"), P"", labelindex("MisClose"));
-- `InnerExp` handles missing expressions by skipping to the next closing
-- parenthesis. A dummy (zero) is returned in place of the expression.
InnerExp = Lc(expect(V"Exp", "ExpExp"), (P(1) - ")")^0 * Cc(0), labelindex("ExpExp"));
}
g = expect(g, "NoExp") * expect(-P(1), "Extra")
-- The `eval` function takes an input string to match against the grammar
-- we've just defined. If the input string matches, then the result of the
-- computation is returned, otherwise we return the error messages and
-- positions of all the failures encountered.
local function eval(input)
local result, label, suffix = g:match(input)
if #errors == 0 then
return result
else
local out = {}
for i, err in ipairs(errors) do
local pos = err[2]
local msg = labels[err[1]][2]
table.insert(out, "syntax error: " .. msg .. " (at index " .. pos .. ")")
end
errors = {}
return nil, table.concat(out, "\n")
end
end
print(eval "98-76*(54/32)")
--> 37.125
print(eval "(1+1-1*2/2")
--> syntax error: missing a closing ')' after the expression (at index 11)
print(eval "(1+)-1*(2/2)")
--> syntax error: expected a term after the operator (at index 4)
print(eval "(1+1)-1*(/2)")
--> syntax error: expected an expression after the parenthesis (at index 10)
print(eval "1+(1-(1*2))/2x")
--> syntax error: extra chracters found after the expression (at index 14)
print(eval "-1+(1-(1*2))/2")
--> syntax error: no expression found (at index 1)
print(eval "(1+1-1*(2/2+)-():")
--> syntax error: expected a term after the operator (at index 13)
--> syntax error: expected an expression after the parenthesis (at index 16)
--> syntax error: missing a closing ')' after the expression (at index 17)
--> syntax error: extra characters found after the expression (at index 17)
|