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.
- coom_lang-0.0.1a1/LICENSE +21 -0
- coom_lang-0.0.1a1/PKG-INFO +37 -0
- coom_lang-0.0.1a1/README.md +15 -0
- coom_lang-0.0.1a1/pyproject.toml +49 -0
- coom_lang-0.0.1a1/pyproject.toml.orig +36 -0
- coom_lang-0.0.1a1/src/coom/compiler/__init__.py +61 -0
- coom_lang-0.0.1a1/src/coom/compiler/_behaviors.py +252 -0
- coom_lang-0.0.1a1/src/coom/compiler/_context.py +83 -0
- coom_lang-0.0.1a1/src/coom/compiler/_declarations.py +226 -0
- coom_lang-0.0.1a1/src/coom/compiler/_expressions.py +336 -0
- coom_lang-0.0.1a1/src/coom/compiler/_testing.py +102 -0
- coom_lang-0.0.1a1/src/coom/compiler/py.typed +0 -0
- coom_lang-0.0.1a1/src/coom/parser/__init__.py +94 -0
- coom_lang-0.0.1a1/src/coom/parser/_antlr/COOMLexer.py +581 -0
- coom_lang-0.0.1a1/src/coom/parser/_antlr/COOMParser.py +5045 -0
- coom_lang-0.0.1a1/src/coom/parser/_antlr/COOMParserBase.py +16 -0
- coom_lang-0.0.1a1/src/coom/parser/_antlr/COOMVisitor.py +358 -0
- coom_lang-0.0.1a1/src/coom/parser/_antlr/__init__.py +0 -0
- coom_lang-0.0.1a1/src/coom/parser/py.typed +0 -0
- coom_lang-0.0.1a1/src/coom/writer/__init__.py +429 -0
- coom_lang-0.0.1a1/src/coom/writer/py.typed +0 -0
|
@@ -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())
|