coom-lang 0.0.1a1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 denkbares GmbH
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,37 @@
1
+ Metadata-Version: 2.4
2
+ Name: coom-lang
3
+ Version: 0.0.1a1
4
+ Summary: Parser, compiler and writer of the COOM product configuration language
5
+ Keywords: coom,product configuration,dsl,parser,compiler
6
+ Author: denkbares GmbH
7
+ Author-email: denkbares GmbH <info@denkbares.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 2 - Pre-Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Requires-Dist: coom-core==0.0.1a1
16
+ Requires-Dist: antlr4-python3-runtime==4.13.2
17
+ Requires-Python: >=3.12
18
+ Project-URL: Homepage, https://www.coom-lang.org
19
+ Project-URL: Repository, https://github.com/denkbares/coom-lang
20
+ Project-URL: Changelog, https://github.com/denkbares/coom-lang/blob/main/CHANGELOG.md
21
+ Description-Content-Type: text/markdown
22
+
23
+ # coom-lang
24
+
25
+ Parser, compiler and writer of the [COOM](https://www.coom-lang.org) product configuration language.
26
+
27
+ ```python
28
+ from coom.compiler import compile_files
29
+
30
+ result = compile_files(["Bike.model"])
31
+ for diagnostic in result.diagnostics:
32
+ print(diagnostic)
33
+ model = result.model
34
+ ```
35
+
36
+ The compiled model is the one of `coom-core`. Sources live in the
37
+ [coom-lang repository](https://github.com/denkbares/coom-lang) under `packages/coom-lang`.
@@ -0,0 +1,15 @@
1
+ # coom-lang
2
+
3
+ Parser, compiler and writer of the [COOM](https://www.coom-lang.org) product configuration language.
4
+
5
+ ```python
6
+ from coom.compiler import compile_files
7
+
8
+ result = compile_files(["Bike.model"])
9
+ for diagnostic in result.diagnostics:
10
+ print(diagnostic)
11
+ model = result.model
12
+ ```
13
+
14
+ The compiled model is the one of `coom-core`. Sources live in the
15
+ [coom-lang repository](https://github.com/denkbares/coom-lang) under `packages/coom-lang`.
@@ -0,0 +1,49 @@
1
+ [build-system]
2
+ requires = ["uv_build>=0.13.6,<0.14"]
3
+ build-backend = "uv_build"
4
+
5
+ [project]
6
+ name = "coom-lang"
7
+ version = "0.0.1a1"
8
+ description = "Parser, compiler and writer of the COOM product configuration language"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ requires-python = ">=3.12"
13
+ keywords = [
14
+ "coom",
15
+ "product configuration",
16
+ "dsl",
17
+ "parser",
18
+ "compiler",
19
+ ]
20
+ classifiers = [
21
+ "Development Status :: 2 - Pre-Alpha",
22
+ "Intended Audience :: Developers",
23
+ "Programming Language :: Python :: 3",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Programming Language :: Python :: 3.13",
26
+ ]
27
+ dependencies = [
28
+ "coom-core==0.0.1a1",
29
+ "antlr4-python3-runtime==4.13.2",
30
+ ]
31
+
32
+ [[project.authors]]
33
+ name = "denkbares GmbH"
34
+ email = "info@denkbares.com"
35
+
36
+ [project.urls]
37
+ Homepage = "https://www.coom-lang.org"
38
+ Repository = "https://github.com/denkbares/coom-lang"
39
+ Changelog = "https://github.com/denkbares/coom-lang/blob/main/CHANGELOG.md"
40
+
41
+ [tool.uv.build-backend]
42
+ module-name = [
43
+ "coom.parser",
44
+ "coom.compiler",
45
+ "coom.writer",
46
+ ]
47
+
48
+ [tool.uv.sources.coom-core]
49
+ workspace = true
@@ -0,0 +1,36 @@
1
+ [build-system]
2
+ requires = ["uv_build>=0.13.6,<0.14"]
3
+ build-backend = "uv_build"
4
+
5
+ [project]
6
+ name = "coom-lang"
7
+ version = "0.0.1a1"
8
+ description = "Parser, compiler and writer of the COOM product configuration language"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ requires-python = ">=3.12"
13
+ authors = [{ name = "denkbares GmbH", email = "info@denkbares.com" }]
14
+ keywords = ["coom", "product configuration", "dsl", "parser", "compiler"]
15
+ classifiers = [
16
+ "Development Status :: 2 - Pre-Alpha",
17
+ "Intended Audience :: Developers",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.12",
20
+ "Programming Language :: Python :: 3.13",
21
+ ]
22
+ dependencies = [
23
+ "coom-core==0.0.1a1",
24
+ "antlr4-python3-runtime==4.13.2",
25
+ ]
26
+
27
+ [project.urls]
28
+ Homepage = "https://www.coom-lang.org"
29
+ Repository = "https://github.com/denkbares/coom-lang"
30
+ Changelog = "https://github.com/denkbares/coom-lang/blob/main/CHANGELOG.md"
31
+
32
+ [tool.uv.build-backend]
33
+ module-name = ["coom.parser", "coom.compiler", "coom.writer"]
34
+
35
+ [tool.uv.sources]
36
+ coom-core = { workspace = true }
@@ -0,0 +1,61 @@
1
+ """Compiles parse trees into a model and reports the errors it finds with their source positions.
2
+
3
+ The compiler resolves names and builds the model graph, then runs the static checks of coom.check on it. It continues
4
+ after errors, leaving out the statements that contain them, so tools still get a model.
5
+ """
6
+
7
+ from collections.abc import Iterable
8
+ from dataclasses import dataclass
9
+ from pathlib import Path
10
+
11
+ from coom.check import check
12
+ from coom.compiler._behaviors import compile_behaviors
13
+ from coom.compiler._context import Compilation
14
+ from coom.compiler._declarations import assign_attribute_values, declare_fields, declare_types
15
+ from coom.compiler._testing import compile_tests
16
+ from coom.diagnostics import Diagnostic, Severity
17
+ from coom.model import Model
18
+ from coom.parser import ParseResult, parse, parse_file
19
+
20
+ __all__ = ["CompileResult", "compile", "compile_files", "compile_text"]
21
+
22
+ # Each phase runs over all sources before the next one starts, so declarations may refer to any type in any source.
23
+ _PHASES = (declare_types, declare_fields, assign_attribute_values, compile_behaviors, compile_tests)
24
+
25
+
26
+ @dataclass(frozen=True, slots=True)
27
+ class CompileResult:
28
+ model: Model
29
+ diagnostics: tuple[Diagnostic, ...]
30
+ """The syntax errors of all sources, then the errors of compilation, then those of the static checks."""
31
+
32
+ @property
33
+ def ok(self) -> bool:
34
+ return not any(diagnostic.severity is Severity.ERROR for diagnostic in self.diagnostics)
35
+
36
+
37
+ def compile(*sources: ParseResult, checks: bool = True) -> CompileResult:
38
+ """Compiles the parse results of one or more sources that together form one model."""
39
+ compilation = Compilation()
40
+ for phase in _PHASES:
41
+ for source in sources:
42
+ compilation.source = source.source
43
+ phase(compilation, source.tree)
44
+ model = Model(
45
+ root=compilation.root,
46
+ structures=tuple(compilation.structures),
47
+ enumerations=tuple(compilation.enumerations),
48
+ behaviors=tuple(compilation.behaviors),
49
+ tests=tuple(compilation.tests),
50
+ )
51
+ syntax = tuple(diagnostic for source in sources for diagnostic in source.diagnostics)
52
+ checked = check(model) if checks else ()
53
+ return CompileResult(model, (*syntax, *compilation.diagnostics, *checked))
54
+
55
+
56
+ def compile_text(text: str, *, source: str | None = None) -> CompileResult:
57
+ return compile(parse(text, source=source))
58
+
59
+
60
+ def compile_files(paths: Iterable[str | Path]) -> CompileResult:
61
+ return compile(*(parse_file(path) for path in paths))
@@ -0,0 +1,252 @@
1
+ """The phase that compiles behavior blocks into flat behaviors.
2
+
3
+ Definitions are substituted, preconditions of enclosing blocks are collected into each statement, and derivation
4
+ tables are lowered to one require or default per row and column.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import contextlib
10
+ from dataclasses import dataclass
11
+ from typing import Any, TypedDict
12
+
13
+ from coom.compiler._context import Abort, Compilation, children, name, need, unquote
14
+ from coom.compiler._expressions import Expressions, Scope
15
+ from coom.model import (
16
+ AttributeStep,
17
+ Cell,
18
+ CellAtom,
19
+ Combinations,
20
+ CompareOp,
21
+ Condition,
22
+ Default,
23
+ Direction,
24
+ Directive,
25
+ Enumeration,
26
+ Formula,
27
+ Imply,
28
+ Optimize,
29
+ Or,
30
+ PathAccess,
31
+ Require,
32
+ Row,
33
+ RowKind,
34
+ Span,
35
+ Structure,
36
+ Visibility,
37
+ value_type,
38
+ )
39
+ from coom.parser._antlr.COOMParser import COOMParser
40
+
41
+
42
+ def compile_behaviors(compilation: Compilation, tree: Any) -> None:
43
+ for node in children(tree):
44
+ if isinstance(node, COOMParser.BehaviorContext):
45
+ try:
46
+ origin = resolve_origin(compilation, node.name())
47
+ block = need(node.behavior_block())
48
+ except Abort:
49
+ continue
50
+ _Block(compilation, Expressions(compilation, origin)).block(block, Scope.of(compilation, origin), _Frame())
51
+
52
+
53
+ def resolve_origin(compilation: Compilation, node: Any) -> Structure:
54
+ """Returns the structure a behavior or test block names, the product if it names none."""
55
+ if node is None:
56
+ return compilation.root
57
+ type_name = name(node)
58
+ type_ = compilation.types.get(type_name)
59
+ if type_ is None:
60
+ compilation.fail(node, f"the structure {type_name} is not defined")
61
+ if not isinstance(type_, Structure):
62
+ compilation.fail(node, f"{type_name} is an enumeration, not a structure")
63
+ return type_
64
+
65
+
66
+ @dataclass(frozen=True, slots=True)
67
+ class _Frame:
68
+ """What a statement inherits from its enclosing blocks."""
69
+
70
+ conditions: tuple[Condition, ...] = ()
71
+ explanation: str | None = None
72
+
73
+
74
+ class _Common(TypedDict):
75
+ origin: Structure
76
+ conditions: tuple[Condition, ...]
77
+ explanation: str | None
78
+ span: Span
79
+
80
+
81
+ class _Block:
82
+ def __init__(self, compilation: Compilation, expressions: Expressions) -> None:
83
+ self.compilation = compilation
84
+ self.expressions = expressions
85
+ self.origin = expressions.origin
86
+
87
+ def block(self, node: Any, scope: Scope, frame: _Frame) -> None:
88
+ for definition in node.define():
89
+ try:
90
+ formula = self.expressions.formula(definition.formula(), scope)
91
+ scope = scope.define(name(definition.name()), formula)
92
+ except Abort:
93
+ pass
94
+ for statement in node.conditioned():
95
+ with contextlib.suppress(Abort):
96
+ self.conditioned(statement, scope, frame)
97
+
98
+ def conditioned(self, node: Any, scope: Scope, frame: _Frame) -> None:
99
+ explanation = frame.explanation
100
+ if node.explanation() is not None:
101
+ explanation = unquote(need(node.explanation().name()).getText())
102
+ preconditions = tuple(self.expressions.condition(p.condition(), scope) for p in node.precondition())
103
+ frame = _Frame((*frame.conditions, *preconditions), explanation)
104
+ if node.behavior_block() is not None:
105
+ self.block(node.behavior_block(), scope, frame)
106
+ elif node.require() is not None:
107
+ self._require(node.require(), scope, frame)
108
+ elif node.assign_imply() is not None:
109
+ self._imply(node.assign_imply(), scope, frame)
110
+ elif node.assign_default() is not None:
111
+ self._default(node.assign_default(), scope, frame)
112
+ elif node.optimize() is not None:
113
+ self._optimize(node.optimize(), scope, frame)
114
+ elif node.visibility() is not None:
115
+ self._visibility(node.visibility(), scope, frame)
116
+ elif node.combinations() is not None:
117
+ self._combinations(node.combinations(), scope, frame)
118
+ elif node.derivation_table() is not None:
119
+ self._derivation_table(node.derivation_table(), scope, frame)
120
+
121
+ def _common(self, node: Any, frame: _Frame) -> _Common:
122
+ return _Common(
123
+ origin=self.origin,
124
+ conditions=frame.conditions,
125
+ explanation=frame.explanation,
126
+ span=self.compilation.span(node),
127
+ )
128
+
129
+ def _require(self, node: Any, scope: Scope, frame: _Frame) -> None:
130
+ condition = self.expressions.condition(node.condition(), scope)
131
+ self.compilation.behaviors.append(Require(condition=condition, **self._common(node, frame)))
132
+
133
+ def _imply(self, node: Any, scope: Scope, frame: _Frame) -> None:
134
+ target = self.expressions.assignable(node.path(), scope)
135
+ value = self.expressions.formula(node.formula(), scope.expecting(target.type))
136
+ self.compilation.behaviors.append(Imply(target=target, value=value, **self._common(node, frame)))
137
+
138
+ def _default(self, node: Any, scope: Scope, frame: _Frame) -> None:
139
+ priority = None
140
+ if node.priority is not None:
141
+ priority = int(node.priority.text)
142
+ if priority < 1:
143
+ self.compilation.fail(node.priority, "the priority must be at least 1")
144
+ target = self.expressions.assignable(node.path(), scope)
145
+ value = self.expressions.formula(node.formula(), scope.expecting(target.type))
146
+ self.compilation.behaviors.append(
147
+ Default(target=target, value=value, priority=priority, **self._common(node, frame))
148
+ )
149
+
150
+ def _optimize(self, node: Any, scope: Scope, frame: _Frame) -> None:
151
+ target = self.expressions.assignable(node.path(), scope)
152
+ direction = Direction(need(node.kw).text)
153
+ self.compilation.behaviors.append(Optimize(direction=direction, target=target, **self._common(node, frame)))
154
+
155
+ def _visibility(self, node: Any, scope: Scope, frame: _Frame) -> None:
156
+ directive = Directive(need(node.directive).text)
157
+ target = self.expressions.assignable(node.path(), scope)
158
+ options = []
159
+ if node.name():
160
+ enumeration = target.type
161
+ if not isinstance(enumeration, Enumeration):
162
+ self.compilation.fail(node.path(), "only options of an enumeration can be hidden individually")
163
+ if directive is not Directive.HIDE:
164
+ self.compilation.fail(node.directive, "options can only be hidden, not made read only")
165
+ for option_node in node.name():
166
+ try:
167
+ option = enumeration.option(name(option_node))
168
+ except KeyError:
169
+ self.compilation.fail(
170
+ option_node, f"{option_node.getText()} is not an option of {enumeration.name}"
171
+ )
172
+ if option not in options:
173
+ options.append(option)
174
+ if len(options) == len(enumeration.options):
175
+ self.compilation.fail(node, f"cannot hide all options of {enumeration.name}")
176
+ self.compilation.behaviors.append(
177
+ Visibility(directive=directive, target=target, options=tuple(options), **self._common(node, frame))
178
+ )
179
+
180
+ def _combinations(self, node: Any, scope: Scope, frame: _Frame) -> None:
181
+ if not node.combination_row():
182
+ self.compilation.fail(node.kw, "a combinations table needs at least one row")
183
+ columns = tuple(self.expressions.formula(column, scope) for column in node.formula())
184
+ rows = []
185
+ for row_node in node.combination_row():
186
+ items = row_node.combination_item()
187
+ if len(items) != len(columns):
188
+ self.compilation.error(row_node, f"the row has {len(items)} cells for {len(columns)} columns")
189
+ continue
190
+ try:
191
+ cells = tuple(self._cell(column, item, scope) for column, item in zip(columns, items, strict=True))
192
+ except Abort:
193
+ continue
194
+ kind = RowKind(need(row_node.rowType).text)
195
+ rows.append(Row(kind, cells, span=self.compilation.span(row_node)))
196
+ self.compilation.behaviors.append(Combinations(columns=columns, rows=tuple(rows), **self._common(node, frame)))
197
+
198
+ def _cell(self, column: Any, node: Any, scope: Scope) -> Cell | None:
199
+ if node.any_ is not None:
200
+ return None
201
+ cell_scope = scope.expecting(value_type(column))
202
+ atoms = []
203
+ for atom in node.combination_atom():
204
+ value = self.expressions.formula(atom.formula(), cell_scope)
205
+ operator = CompareOp.EQ if atom.operator is None else CompareOp(atom.operator.getText())
206
+ atoms.append(CellAtom(value, operator, span=self.compilation.span(atom)))
207
+ return tuple(atoms)
208
+
209
+ def _derivation_table(self, node: Any, scope: Scope, frame: _Frame) -> None:
210
+ kind = need(node.kw).text
211
+ targets = tuple(self.expressions.formula(f, scope) for f in need(node.assignment_columns).formula())
212
+ columns: tuple[Formula, ...] = ()
213
+ if node.condition_columns is not None:
214
+ columns = tuple(self.expressions.formula(f, scope) for f in node.condition_columns.formula())
215
+ if kind == "default":
216
+ for target, target_node in zip(targets, node.assignment_columns.formula(), strict=True):
217
+ path = target.path if isinstance(target, PathAccess) else None
218
+ if path is None or not path.steps or isinstance(path.steps[-1], AttributeStep):
219
+ self.compilation.fail(target_node, "a column of a default table must be a path to a feature")
220
+ for row in node.derivation_table_row():
221
+ with contextlib.suppress(Abort):
222
+ self._derivation_row(kind, targets, columns, row, scope, frame)
223
+
224
+ def _derivation_row(
225
+ self, kind: str, targets: tuple[Any, ...], columns: tuple[Any, ...], node: Any, scope: Scope, frame: _Frame
226
+ ) -> None:
227
+ cells = node.derivation_condition_cell()
228
+ values = node.derivation_table_atom()
229
+ if len(cells) != len(columns):
230
+ self.compilation.fail(node, f"the row has {len(cells)} condition cells for {len(columns)} columns")
231
+ if len(values) != len(targets):
232
+ self.compilation.fail(node, f"the row has {len(values)} values for {len(targets)} columns")
233
+ # A row reads as a set of statements that share the conditions of its cells, a cell holding if any atom holds.
234
+ conditions = list(frame.conditions)
235
+ for column, cell in zip(columns, cells, strict=True):
236
+ if cell.any_ is not None or not cell.derivation_table_atom():
237
+ continue
238
+ atoms = tuple(
239
+ self.expressions.compare_cell(column, atom.operator, atom.formula(), scope)
240
+ for atom in cell.derivation_table_atom()
241
+ )
242
+ conditions.append(atoms[0] if len(atoms) == 1 else Or(atoms, span=self.compilation.span(cell)))
243
+ common = self._common(node, _Frame(tuple(conditions), frame.explanation))
244
+ for target, value_node in zip(targets, values, strict=True):
245
+ compare = self.expressions.compare_cell(target, value_node.operator, value_node.formula(), scope)
246
+ if kind == "require":
247
+ self.compilation.behaviors.append(Require(condition=compare, **common))
248
+ elif compare.op is not CompareOp.EQ:
249
+ self.compilation.fail(value_node, "a default table only assigns values with =")
250
+ else:
251
+ assert isinstance(target, PathAccess)
252
+ self.compilation.behaviors.append(Default(target=target.path, value=compare.right, **common))
@@ -0,0 +1,83 @@
1
+ """State shared by the compiler phases and helpers to report errors at parse tree positions."""
2
+
3
+ import re
4
+ from dataclasses import dataclass, field
5
+ from typing import Any, NoReturn
6
+
7
+ from antlr4 import ParserRuleContext
8
+ from antlr4.tree.Tree import ErrorNode
9
+
10
+ from coom.diagnostics import Diagnostic, Severity
11
+ from coom.model import ROOT_NAME, Behavior, Enumeration, Option, Span, Structure, TestCase
12
+ from coom.parser import token_span
13
+
14
+
15
+ class Abort(Exception):
16
+ """Stops compiling the current statement, whose error is already reported or is a syntax error."""
17
+
18
+
19
+ @dataclass(eq=False)
20
+ class Compilation:
21
+ """The model under construction and the diagnostics collected so far."""
22
+
23
+ source: str | None = None
24
+ """The source of the parse tree the current phase works on."""
25
+ root: Structure = field(default_factory=lambda: Structure(name=ROOT_NAME))
26
+ structures: list[Structure] = field(default_factory=list)
27
+ enumerations: list[Enumeration] = field(default_factory=list)
28
+ behaviors: list[Behavior] = field(default_factory=list)
29
+ tests: list[TestCase] = field(default_factory=list)
30
+ diagnostics: list[Diagnostic] = field(default_factory=list)
31
+ types: dict[str, Structure | Enumeration] = field(default_factory=dict)
32
+ declared: dict[Any, Structure | Enumeration] = field(default_factory=dict)
33
+ """The type declared by each product, structure and enumeration block, keyed by its parse tree node."""
34
+ options: dict[Any, Option] = field(default_factory=dict)
35
+ """The option declared by each option node, which the attribute values phase completes."""
36
+ product: Any = None
37
+ """The first product block, any further one is an error."""
38
+
39
+ def span(self, node: Any) -> Span:
40
+ if isinstance(node, ParserRuleContext):
41
+ stop = node.stop
42
+ if stop is None or stop.tokenIndex < node.start.tokenIndex:
43
+ stop = node.start
44
+ return token_span(node.start, stop, self.source)
45
+ return token_span(node, source=self.source)
46
+
47
+ def error(self, node: Any, message: str) -> None:
48
+ self.diagnostics.append(Diagnostic(Severity.ERROR, message, self.span(node)))
49
+
50
+ def fail(self, node: Any, message: str) -> NoReturn:
51
+ self.error(node, message)
52
+ raise Abort
53
+
54
+
55
+ def need[T](node: T | None) -> T:
56
+ """Returns a parse tree node that error recovery may have left out, aborting the statement if it is missing."""
57
+ if node is None or isinstance(node, ErrorNode):
58
+ raise Abort
59
+ return node
60
+
61
+
62
+ def children(node: Any) -> list[Any]:
63
+ return list(node.children or ()) if node is not None else []
64
+
65
+
66
+ _ESCAPE = re.compile(r"\\(u[0-9a-fA-F]{4}|.)")
67
+ _ESCAPES = {'"': '"', "\\": "\\", "/": "/", "b": "\b", "f": "\f", "n": "\n", "r": "\r", "t": "\t"}
68
+
69
+
70
+ def unquote(text: str) -> str:
71
+ """Returns the text of a quoted name with its escape sequences resolved, other names unchanged."""
72
+ if len(text) < 2 or text[0] not in "'\"" or text[-1] != text[0]:
73
+ return text
74
+
75
+ def resolve(match: re.Match[str]) -> str:
76
+ escape = match.group(1)
77
+ return chr(int(escape[1:], 16)) if escape[0] == "u" and len(escape) == 5 else _ESCAPES.get(escape, escape)
78
+
79
+ return _ESCAPE.sub(resolve, text[1:-1])
80
+
81
+
82
+ def name(node: Any) -> str:
83
+ return unquote(need(node).getText())