OILS / osh / tdop.py View on Github | oilshell.org

338 lines, 141 significant
1"""
2tdop.py - Library for expression parsing.
3"""
4
5from _devbuild.gen.id_kind_asdl import Id, Id_t
6from _devbuild.gen.syntax_asdl import (loc, arith_expr, arith_expr_e,
7 arith_expr_t, word_t, CompoundWord)
8from core.error import p_die
9from core import ui
10from mycpp import mylib
11from mycpp.mylib import tagswitch
12from osh import word_
13
14from typing import (Callable, List, Dict, Tuple, Any, cast, TYPE_CHECKING)
15
16if TYPE_CHECKING: # break circular dep
17 from osh.word_parse import WordParser
18 from core import optview
19 LeftFunc = Callable[['TdopParser', word_t, arith_expr_t, int],
20 arith_expr_t]
21 NullFunc = Callable[['TdopParser', word_t, int], arith_expr_t]
22
23
24def IsIndexable(node):
25 # type: (arith_expr_t) -> bool
26 """In POSIX shell arith, a[1] is allowed but a[1][1] isn't.
27
28 We also allow $a[i] and foo$x[i] (formerly parse_dynamic_arith)
29 """
30 with tagswitch(node) as case:
31 if case(arith_expr_e.VarSub, arith_expr_e.Word):
32 return True
33 return False
34
35
36def CheckLhsExpr(node, blame_word):
37 # type: (arith_expr_t, word_t) -> None
38 """Determine if a node is a valid L-value by whitelisting tags.
39
40 Valid:
41 x = y
42 a[1] = y
43 Invalid:
44 a[0][0] = y
45 """
46 UP_node = node
47 if node.tag() == arith_expr_e.Binary:
48 node = cast(arith_expr.Binary, UP_node)
49 if node.op_id == Id.Arith_LBracket and IsIndexable(node.left):
50 return
51 # But a[0][0] = 1 is NOT valid.
52
53 if IsIndexable(node):
54 return
55
56 p_die("Left-hand side of this assignment is invalid", loc.Word(blame_word))
57
58
59#
60# Null Denotation
61#
62
63
64def NullError(p, t, bp):
65 # type: (TdopParser, word_t, int) -> arith_expr_t
66 # TODO: I need position information
67 p_die("Token can't be used in prefix position", loc.Word(t))
68 return None # never reached
69
70
71def NullConstant(p, w, bp):
72 # type: (TdopParser, word_t, int) -> arith_expr_t
73 name_tok = word_.LooksLikeArithVar(w)
74 if name_tok:
75 return name_tok
76
77 # Id.Word_Compound in the spec ensures this cast is valid
78 return cast(CompoundWord, w)
79
80
81def NullParen(p, t, bp):
82 # type: (TdopParser, word_t, int) -> arith_expr_t
83 """Arithmetic grouping."""
84 r = p.ParseUntil(bp)
85 p.Eat(Id.Arith_RParen)
86 return r
87
88
89def NullPrefixOp(p, w, bp):
90 # type: (TdopParser, word_t, int) -> arith_expr_t
91 """Prefix operator.
92
93 Low precedence: return, raise, etc.
94 return x+y is return (x+y), not (return x) + y
95
96 High precedence: logical negation, bitwise complement, etc.
97 !x && y is (!x) && y, not !(x && y)
98 """
99 right = p.ParseUntil(bp)
100 return arith_expr.Unary(word_.ArithId(w), right)
101
102
103#
104# Left Denotation
105#
106
107
108def LeftError(p, t, left, rbp):
109 # type: (TdopParser, word_t, arith_expr_t, int) -> arith_expr_t
110 # Hm is this not called because of binding power?
111 p_die("Token can't be used in infix position", loc.Word(t))
112 return None # never reached
113
114
115def LeftBinaryOp(p, w, left, rbp):
116 # type: (TdopParser, word_t, arith_expr_t, int) -> arith_expr_t
117 """Normal binary operator like 1+2 or 2*3, etc."""
118 # TODO: w should be a Token, and we should extract the token from it.
119 return arith_expr.Binary(word_.ArithId(w), left, p.ParseUntil(rbp))
120
121
122def LeftAssign(p, w, left, rbp):
123 # type: (TdopParser, word_t, arith_expr_t, int) -> arith_expr_t
124 """Normal binary operator like 1+2 or 2*3, etc."""
125 # x += 1, or a[i] += 1
126
127 CheckLhsExpr(left, w)
128 return arith_expr.BinaryAssign(word_.ArithId(w), left, p.ParseUntil(rbp))
129
130
131#
132# Parser definition
133# TODO: To be consistent, move this to osh/tdop_def.py.
134#
135
136if mylib.PYTHON:
137
138 def _ModuleAndFuncName(f):
139 # type: (Any) -> Tuple[str, str]
140 namespace = f.__module__.split('.')[-1]
141 return namespace, f.__name__
142
143 def _CppFuncName(f):
144 # type: (Any) -> str
145 return '%s::%s' % _ModuleAndFuncName(f)
146
147 class LeftInfo(object):
148 """Row for operator.
149
150 In C++ this should be a big array.
151 """
152
153 def __init__(self, led=None, lbp=0, rbp=0):
154 # type: (LeftFunc, int, int) -> None
155 self.led = led or LeftError
156 self.lbp = lbp
157 self.rbp = rbp
158
159 def __str__(self):
160 # type: () -> str
161 """Used by C++ code generation."""
162 return '{ %s, %d, %d },' % (_CppFuncName(
163 self.led), self.lbp, self.rbp)
164
165 def ModuleAndFuncName(self):
166 # type: () -> Tuple[str, str]
167 """Used by C++ code generation."""
168 return _ModuleAndFuncName(self.led)
169
170 class NullInfo(object):
171 """Row for operator.
172
173 In C++ this should be a big array.
174 """
175
176 def __init__(self, nud=None, bp=0):
177 # type: (NullFunc, int) -> None
178 self.nud = nud or LeftError
179 self.bp = bp
180
181 def __str__(self):
182 # type: () -> str
183 """Used by C++ code generation."""
184 return '{ %s, %d },' % (_CppFuncName(self.nud), self.bp)
185
186 def ModuleAndFuncName(self):
187 # type: () -> Tuple[str, str]
188 """Used by C++ code generation."""
189 return _ModuleAndFuncName(self.nud)
190
191 class ParserSpec(object):
192 """Specification for a TDOP parser.
193
194 This can be compiled to a table in C++.
195 """
196
197 def __init__(self):
198 # type: () -> None
199 self.nud_lookup = {} # type: Dict[Id_t, NullInfo]
200 self.led_lookup = {} # type: Dict[Id_t, LeftInfo]
201
202 def Null(self, bp, nud, tokens):
203 # type: (int, NullFunc, List[Id_t]) -> None
204 """Register a token that doesn't take anything on the left.
205
206 Examples: constant, prefix operator, error.
207 """
208 for token in tokens:
209 self.nud_lookup[token] = NullInfo(nud=nud, bp=bp)
210 if token not in self.led_lookup:
211 self.led_lookup[token] = LeftInfo() # error
212
213 def _RegisterLed(self, lbp, rbp, led, tokens):
214 # type: (int, int, LeftFunc, List[Id_t]) -> None
215 for token in tokens:
216 if token not in self.nud_lookup:
217 self.nud_lookup[token] = NullInfo(NullError)
218 self.led_lookup[token] = LeftInfo(lbp=lbp, rbp=rbp, led=led)
219
220 def Left(self, bp, led, tokens):
221 # type: (int, LeftFunc, List[Id_t]) -> None
222 """Register a token that takes an expression on the left."""
223 self._RegisterLed(bp, bp, led, tokens)
224
225 def LeftRightAssoc(self, bp, led, tokens):
226 # type: (int, LeftFunc, List[Id_t]) -> None
227 """Register a right associative operator."""
228 self._RegisterLed(bp, bp - 1, led, tokens)
229
230 def LookupNud(self, token):
231 # type: (Id_t) -> NullInfo
232
233 # As long as the table is complete, this shouldn't fail
234 return self.nud_lookup[token]
235
236 def LookupLed(self, token):
237 # type: (Id_t) -> LeftInfo
238 """Get a left_info for the token."""
239
240 # As long as the table is complete, this shouldn't fail
241 return self.led_lookup[token]
242
243
244class TdopParser(object):
245
246 def __init__(self, spec, w_parser, parse_opts):
247 # type: (ParserSpec, WordParser, optview.Parse) -> None
248 self.spec = spec
249 self.w_parser = w_parser
250 self.parse_opts = parse_opts
251
252 # NOTE: Next() overwrites this state, so we don't need a Reset() method in
253 # between reuses of this TdopParser instance.
254 self.cur_word = None # type: word_t # current token
255 self.op_id = Id.Undefined_Tok
256
257 def CurrentId(self):
258 # type: () -> Id_t
259 """Glue used by the WordParser to check for extra tokens."""
260 return word_.CommandId(self.cur_word)
261
262 def AtToken(self, token_type):
263 # type: (Id_t) -> bool
264 return self.op_id == token_type
265
266 def Eat(self, token_type):
267 # type: (Id_t) -> None
268 """Assert that we're at the current token and advance."""
269 if not self.AtToken(token_type):
270 p_die(
271 'Parser expected %s, got %s' %
272 (ui.PrettyId(token_type), ui.PrettyId(self.op_id)),
273 loc.Word(self.cur_word))
274 self.Next()
275
276 def Next(self):
277 # type: () -> bool
278 self.cur_word = self.w_parser.ReadArithWord()
279 self.op_id = word_.ArithId(self.cur_word)
280 return True
281
282 def ParseUntil(self, rbp):
283 # type: (int) -> arith_expr_t
284 """Parse to the right, eating tokens until we encounter a token with
285 binding power LESS THAN OR EQUAL TO rbp."""
286 # TODO: use Kind.Eof
287 if self.op_id in (Id.Eof_Real, Id.Eof_RParen, Id.Eof_Backtick):
288 p_die('Unexpected end of input', loc.Word(self.cur_word))
289
290 t = self.cur_word
291 null_info = self.spec.LookupNud(self.op_id)
292
293 self.Next() # skip over the token, e.g. ! ~ + -
294 node = null_info.nud(self, t, null_info.bp)
295
296 while True:
297 t = self.cur_word
298 left_info = self.spec.LookupLed(self.op_id)
299
300 # Examples:
301 # If we see 1*2+ , rbp = 27 and lbp = 25, so stop.
302 # If we see 1+2+ , rbp = 25 and lbp = 25, so stop.
303 # If we see 1**2**, rbp = 26 and lbp = 27, so keep going.
304 if rbp >= left_info.lbp:
305 break
306 self.Next() # skip over the token, e.g. / *
307
308 node = left_info.led(self, t, node, left_info.rbp)
309
310 return node
311
312 def Parse(self):
313 # type: () -> arith_expr_t
314
315 self.Next() # may raise ParseError
316
317 if not self.parse_opts.parse_sh_arith():
318 # Affects:
319 # echo $(( x ))
320 # ${a[i]} which should be $[a[i]] -- could have better error
321 #
322 # Note: sh_expr_eval.UnsafeArith has a dynamic e_die() check
323 #
324 # Doesn't affect:
325 # printf -v x # unsafe_arith.ParseLValue
326 # unset x # unsafe_arith.ParseLValue
327 # ${!ref} # unsafe_arith.ParseVarRef
328 # declare -n # not fully implemented yet
329 #
330 # a[i+1]= # parse_sh_assign
331 #
332 # (( a = 1 )) # parse_dparen
333 # for (( i = 0; ... # parse_dparen
334
335 p_die("POSIX shell arithmetic isn't allowed (parse_sh_arith)",
336 loc.Word(self.cur_word))
337
338 return self.ParseUntil(0)