Coverage for klayout_pex/netlist/expression/parser.py: 100%

116 statements  

« prev     ^ index     » next       coverage.py v7.17.2, created at 2026-11-06 13:14 +0000

1# 

2# -------------------------------------------------------------------------------- 

3# SPDX-FileCopyrightText: 2024-2026 Martin Jan Köhler and Harald Pretl 

4# Johannes Kepler University, Institute for Integrated Circuits. 

5# 

6# This file is part of KPEX 

7# (see https://github.com/iic-jku/klayout-pex). 

8# 

9# This program is free software: you can redistribute it and/or modify 

10# it under the terms of the GNU General Public License as published by 

11# the Free Software Foundation, either version 3 of the License, or 

12# (at your option) any later version. 

13# 

14# This program is distributed in the hope that it will be useful, 

15# but WITHOUT ANY WARRANTY; without even the implied warranty of 

16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

17# GNU General Public License for more details. 

18# 

19# You should have received a copy of the GNU General Public License 

20# along with this program. If not, see <http://www.gnu.org/licenses/>. 

21# SPDX-License-Identifier: GPL-3.0-or-later 

22# -------------------------------------------------------------------------------- 

23# 

24 

25""" 

26Parses expressions with the syntax of a netlist flavour (see syntax.py). 

27 

28Besides the operators of the syntax, it reads parentheses, function calls, and the conditional c ? a : b, 

29which binds weakest, from the right (as in C). 

30 

31NOTE: imports neither KLayout nor generated protobuf code, so that format-only paths can use it 

32""" 

33 

34from __future__ import annotations 

35from dataclasses import dataclass 

36import re 

37from typing import * 

38 

39from .nodes import ( 

40 BinaryOperation, 

41 Conditional, 

42 Expression, 

43 ExpressionError, 

44 FunctionCall, 

45 Node, 

46 Number, 

47 Parameter, 

48 UnaryOperation, 

49) 

50from .syntax import Associativity, ExpressionSyntax 

51 

52 

53class ExpressionSyntaxError(ExpressionError): 

54 """ 

55 A text that is no expression of the syntax, e.g. with an unknown function 

56 """ 

57 pass 

58 

59 

60_NAME = r'[A-Za-z_][A-Za-z0-9_]*' 

61_PUNCTUATION = ('(', ')', ',', '?', ':') 

62 

63 

64@dataclass(frozen=True) 

65class _Token: 

66 kind: str # 'number', 'name', 'symbol' or 'end' 

67 text: str 

68 column: int # counted from 1 

69 

70 

71class ExpressionParser: 

72 def __init__(self, syntax: ExpressionSyntax): 

73 self.syntax = syntax 

74 symbols = sorted({*syntax.binary_operators, *syntax.unary_operators, *_PUNCTUATION}, key=len, reverse=True) 

75 self._token_pattern = re.compile(rf"\s*(?:(?P<number>{syntax.number_pattern})" 

76 rf"|(?P<name>{_NAME})" 

77 rf"|(?P<symbol>{'|'.join(re.escape(s) for s in symbols)}))") 

78 

79 def parse(self, text: str) -> Expression: 

80 """ 

81 :raises ExpressionSyntaxError: if the text is no expression of the syntax 

82 """ 

83 return Expression(text=text, root=_Parse(self.syntax, text, self._tokens(text)).expression()) 

84 

85 def _tokens(self, text: str) -> List[_Token]: 

86 tokens: List[_Token] = [] 

87 position = 0 

88 while text[position:].strip(): 

89 match = self._token_pattern.match(text, position) 

90 if match is None: 

91 column = len(text) - len(text[position:].lstrip()) + 1 

92 raise ExpressionSyntaxError(f"{text}: unexpected {text[column - 1]!r} at column {column}") 

93 kind = match.lastgroup 

94 tokens.append(_Token(kind=kind, text=match.group(kind), column=match.start(kind) + 1)) 

95 position = match.end() 

96 tokens.append(_Token(kind='end', text='', column=len(text) + 1)) 

97 return tokens 

98 

99 

100class _Parse: 

101 """ 

102 One expression, parsed by precedence climbing 

103 """ 

104 

105 def __init__(self, syntax: ExpressionSyntax, text: str, tokens: List[_Token]): 

106 self.syntax = syntax 

107 self.text = text 

108 self.tokens = tokens 

109 self.index = 0 

110 

111 def expression(self) -> Node: 

112 node = self.conditional() 

113 if self.peek().kind != 'end': 

114 raise self.unexpected() 

115 return node 

116 

117 # -------------------------------------------------------------------------------- 

118 

119 def peek(self) -> _Token: 

120 return self.tokens[self.index] 

121 

122 def next(self) -> _Token: 

123 token = self.tokens[self.index] 

124 self.index += 1 

125 return token 

126 

127 def accept(self, symbol: str) -> bool: 

128 token = self.peek() 

129 if token.kind == 'symbol' and token.text == symbol: 

130 self.index += 1 

131 return True 

132 return False 

133 

134 def expect(self, symbol: str): 

135 if not self.accept(symbol): 

136 raise self.unexpected(f"{symbol!r} expected") 

137 

138 def unexpected(self, expected: Optional[str] = None) -> ExpressionSyntaxError: 

139 token = self.peek() 

140 found = 'ends early' if token.kind == 'end' else f"unexpected {token.text!r} at column {token.column}" 

141 return ExpressionSyntaxError(f"{self.text}: {found}" + (f", {expected}" if expected else '')) 

142 

143 # -------------------------------------------------------------------------------- 

144 

145 def conditional(self) -> Node: 

146 condition = self.binary(0) 

147 if not self.accept('?'): 

148 return condition 

149 if_true = self.conditional() 

150 self.expect(':') 

151 return Conditional(condition=condition, if_true=if_true, if_false=self.conditional()) 

152 

153 def binary(self, min_precedence: int) -> Node: 

154 node = self.unary() 

155 while True: 

156 token = self.peek() 

157 operator = self.syntax.binary_operators.get(token.text) if token.kind == 'symbol' else None 

158 if operator is None or operator.precedence < min_precedence: 

159 return node 

160 self.next() 

161 right_precedence = operator.precedence + (operator.associativity == Associativity.LEFT) 

162 node = BinaryOperation(operator=operator.operator, left=node, right=self.binary(right_precedence)) 

163 

164 def unary(self) -> Node: 

165 token = self.peek() 

166 operator = self.syntax.unary_operators.get(token.text) if token.kind == 'symbol' else None 

167 if operator is None: 

168 return self.primary() 

169 self.next() 

170 return UnaryOperation(operator=operator, operand=self.binary(self.syntax.unary_precedence)) 

171 

172 def primary(self) -> Node: 

173 token = self.next() 

174 if token.kind == 'number': 

175 return Number(self.syntax.parse_number(token.text)) 

176 if token.kind == 'name': 

177 if self.accept('('): 

178 return self.function_call(token) 

179 return Parameter(self.syntax.name_key(token.text)) 

180 if token.kind == 'symbol' and token.text == '(': 

181 node = self.conditional() 

182 self.expect(')') 

183 return node 

184 self.index -= 1 

185 raise self.unexpected() 

186 

187 def function_call(self, name: _Token) -> Node: 

188 function = self.syntax.functions.get(self.syntax.name_key(name.text)) 

189 if function is None: 

190 raise ExpressionSyntaxError(f"{self.text}: unknown function {name.text} " 

191 f"(known are {', '.join(sorted(self.syntax.functions))})") 

192 arguments: List[Node] = [] 

193 if not self.accept(')'): 

194 arguments.append(self.conditional()) 

195 while self.accept(','): 

196 arguments.append(self.conditional()) 

197 self.expect(')') 

198 if len(arguments) != function.arity: 

199 raise ExpressionSyntaxError(f"{self.text}: {function.name} takes {function.arity} " 

200 f"argument{'s' if function.arity != 1 else ''}, not {len(arguments)}") 

201 return FunctionCall(function=function, arguments=tuple(arguments))