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
« 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#
25"""
26Parses expressions with the syntax of a netlist flavour (see syntax.py).
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).
31NOTE: imports neither KLayout nor generated protobuf code, so that format-only paths can use it
32"""
34from __future__ import annotations
35from dataclasses import dataclass
36import re
37from typing import *
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
53class ExpressionSyntaxError(ExpressionError):
54 """
55 A text that is no expression of the syntax, e.g. with an unknown function
56 """
57 pass
60_NAME = r'[A-Za-z_][A-Za-z0-9_]*'
61_PUNCTUATION = ('(', ')', ',', '?', ':')
64@dataclass(frozen=True)
65class _Token:
66 kind: str # 'number', 'name', 'symbol' or 'end'
67 text: str
68 column: int # counted from 1
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)}))")
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())
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
100class _Parse:
101 """
102 One expression, parsed by precedence climbing
103 """
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
111 def expression(self) -> Node:
112 node = self.conditional()
113 if self.peek().kind != 'end':
114 raise self.unexpected()
115 return node
117 # --------------------------------------------------------------------------------
119 def peek(self) -> _Token:
120 return self.tokens[self.index]
122 def next(self) -> _Token:
123 token = self.tokens[self.index]
124 self.index += 1
125 return token
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
134 def expect(self, symbol: str):
135 if not self.accept(symbol):
136 raise self.unexpected(f"{symbol!r} expected")
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 ''))
143 # --------------------------------------------------------------------------------
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())
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))
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))
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()
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))