flamapy-fm 2.6.0.dev11__tar.gz → 2.9.0.dev0__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.
- {flamapy_fm-2.6.0.dev11 → flamapy_fm-2.9.0.dev0}/PKG-INFO +6 -2
- flamapy_fm-2.9.0.dev0/flamapy/metamodels/fm_metamodel/__init__.py +26 -0
- {flamapy_fm-2.6.0.dev11 → flamapy_fm-2.9.0.dev0}/flamapy/metamodels/fm_metamodel/models/__init__.py +2 -1
- {flamapy_fm-2.6.0.dev11 → flamapy_fm-2.9.0.dev0}/flamapy/metamodels/fm_metamodel/models/clause_set.py +90 -14
- {flamapy_fm-2.6.0.dev11 → flamapy_fm-2.9.0.dev0}/flamapy/metamodels/fm_metamodel/models/feature_model.py +18 -1
- {flamapy_fm-2.6.0.dev11 → flamapy_fm-2.9.0.dev0}/flamapy/metamodels/fm_metamodel/operations/fm_atomic_sets.py +8 -15
- {flamapy_fm-2.6.0.dev11 → flamapy_fm-2.9.0.dev0}/flamapy/metamodels/fm_metamodel/operations/fm_core_features.py +4 -0
- {flamapy_fm-2.6.0.dev11 → flamapy_fm-2.9.0.dev0}/flamapy/metamodels/fm_metamodel/operations/fm_estimated_configurations_number.py +2 -0
- {flamapy_fm-2.6.0.dev11 → flamapy_fm-2.9.0.dev0}/flamapy/metamodels/fm_metamodel/operations/fm_language_level.py +7 -25
- flamapy_fm-2.9.0.dev0/flamapy/metamodels/fm_metamodel/transformations/_uvl_lexer.py +67 -0
- {flamapy_fm-2.6.0.dev11 → flamapy_fm-2.9.0.dev0}/flamapy/metamodels/fm_metamodel/transformations/afm_reader.py +35 -5
- flamapy_fm-2.9.0.dev0/flamapy/metamodels/fm_metamodel/transformations/afm_writer.py +226 -0
- flamapy_fm-2.9.0.dev0/flamapy/metamodels/fm_metamodel/transformations/clafer_writer.py +262 -0
- {flamapy_fm-2.6.0.dev11 → flamapy_fm-2.9.0.dev0}/flamapy/metamodels/fm_metamodel/transformations/featureide_reader.py +30 -16
- flamapy_fm-2.9.0.dev0/flamapy/metamodels/fm_metamodel/transformations/featureide_writer.py +151 -0
- {flamapy_fm-2.6.0.dev11 → flamapy_fm-2.9.0.dev0}/flamapy/metamodels/fm_metamodel/transformations/fm_secure_features_names.py +37 -2
- flamapy_fm-2.9.0.dev0/flamapy/metamodels/fm_metamodel/transformations/glencoe_writer.py +131 -0
- flamapy_fm-2.9.0.dev0/flamapy/metamodels/fm_metamodel/transformations/group_support.py +83 -0
- {flamapy_fm-2.6.0.dev11 → flamapy_fm-2.9.0.dev0}/flamapy/metamodels/fm_metamodel/transformations/json_writer.py +10 -13
- flamapy_fm-2.9.0.dev0/flamapy/metamodels/fm_metamodel/transformations/pl_writer.py +210 -0
- {flamapy_fm-2.6.0.dev11 → flamapy_fm-2.9.0.dev0}/flamapy/metamodels/fm_metamodel/transformations/pysat_to_fm.py +13 -0
- {flamapy_fm-2.6.0.dev11 → flamapy_fm-2.9.0.dev0}/flamapy/metamodels/fm_metamodel/transformations/refactorings/cardinality_group_refactoring.py +71 -2
- {flamapy_fm-2.6.0.dev11 → flamapy_fm-2.9.0.dev0}/flamapy/metamodels/fm_metamodel/transformations/refactorings/multiple_group_decomposition_refactoring.py +16 -5
- {flamapy_fm-2.6.0.dev11 → flamapy_fm-2.9.0.dev0}/flamapy/metamodels/fm_metamodel/transformations/uvl_reader.py +3 -3
- {flamapy_fm-2.6.0.dev11 → flamapy_fm-2.9.0.dev0}/flamapy/metamodels/fm_metamodel/transformations/uvl_writer.py +103 -27
- {flamapy_fm-2.6.0.dev11 → flamapy_fm-2.9.0.dev0}/flamapy/metamodels/fm_metamodel/transformations/xml_writer.py +158 -140
- {flamapy_fm-2.6.0.dev11 → flamapy_fm-2.9.0.dev0}/flamapy_fm.egg-info/PKG-INFO +6 -2
- {flamapy_fm-2.6.0.dev11 → flamapy_fm-2.9.0.dev0}/flamapy_fm.egg-info/SOURCES.txt +3 -0
- flamapy_fm-2.9.0.dev0/flamapy_fm.egg-info/entry_points.txt +2 -0
- {flamapy_fm-2.6.0.dev11 → flamapy_fm-2.9.0.dev0}/flamapy_fm.egg-info/requires.txt +6 -1
- {flamapy_fm-2.6.0.dev11 → flamapy_fm-2.9.0.dev0}/pyproject.toml +13 -2
- flamapy_fm-2.6.0.dev11/flamapy/metamodels/fm_metamodel/__init__.py +0 -0
- flamapy_fm-2.6.0.dev11/flamapy/metamodels/fm_metamodel/transformations/afm_writer.py +0 -140
- flamapy_fm-2.6.0.dev11/flamapy/metamodels/fm_metamodel/transformations/clafer_writer.py +0 -160
- flamapy_fm-2.6.0.dev11/flamapy/metamodels/fm_metamodel/transformations/featureide_writer.py +0 -150
- flamapy_fm-2.6.0.dev11/flamapy/metamodels/fm_metamodel/transformations/glencoe_writer.py +0 -133
- flamapy_fm-2.6.0.dev11/flamapy/metamodels/fm_metamodel/transformations/pl_writer.py +0 -170
- {flamapy_fm-2.6.0.dev11 → flamapy_fm-2.9.0.dev0}/LICENSE +0 -0
- {flamapy_fm-2.6.0.dev11 → flamapy_fm-2.9.0.dev0}/README.md +0 -0
- {flamapy_fm-2.6.0.dev11 → flamapy_fm-2.9.0.dev0}/flamapy/metamodels/fm_metamodel/operations/__init__.py +0 -0
- {flamapy_fm-2.6.0.dev11 → flamapy_fm-2.9.0.dev0}/flamapy/metamodels/fm_metamodel/operations/fm_average_branching_factor.py +0 -0
- {flamapy_fm-2.6.0.dev11 → flamapy_fm-2.9.0.dev0}/flamapy/metamodels/fm_metamodel/operations/fm_count_leafs.py +0 -0
- {flamapy_fm-2.6.0.dev11 → flamapy_fm-2.9.0.dev0}/flamapy/metamodels/fm_metamodel/operations/fm_feature_ancestors.py +0 -0
- {flamapy_fm-2.6.0.dev11 → flamapy_fm-2.9.0.dev0}/flamapy/metamodels/fm_metamodel/operations/fm_generate_random_attribute.py +0 -0
- {flamapy_fm-2.6.0.dev11 → flamapy_fm-2.9.0.dev0}/flamapy/metamodels/fm_metamodel/operations/fm_generate_random_feature_model.py +0 -0
- {flamapy_fm-2.6.0.dev11 → flamapy_fm-2.9.0.dev0}/flamapy/metamodels/fm_metamodel/operations/fm_leaf_features.py +0 -0
- {flamapy_fm-2.6.0.dev11 → flamapy_fm-2.9.0.dev0}/flamapy/metamodels/fm_metamodel/operations/fm_max_depth_tree.py +0 -0
- {flamapy_fm-2.6.0.dev11 → flamapy_fm-2.9.0.dev0}/flamapy/metamodels/fm_metamodel/operations/fm_metrics.py +0 -0
- {flamapy_fm-2.6.0.dev11 → flamapy_fm-2.9.0.dev0}/flamapy/metamodels/fm_metamodel/operations/fm_variation_points.py +0 -0
- {flamapy_fm-2.6.0.dev11 → flamapy_fm-2.9.0.dev0}/flamapy/metamodels/fm_metamodel/operations/interfaces/__init__.py +0 -0
- {flamapy_fm-2.6.0.dev11 → flamapy_fm-2.9.0.dev0}/flamapy/metamodels/fm_metamodel/operations/interfaces/variation_points.py +0 -0
- {flamapy_fm-2.6.0.dev11 → flamapy_fm-2.9.0.dev0}/flamapy/metamodels/fm_metamodel/transformations/__init__.py +0 -0
- {flamapy_fm-2.6.0.dev11 → flamapy_fm-2.9.0.dev0}/flamapy/metamodels/fm_metamodel/transformations/flat_fm.py +0 -0
- {flamapy_fm-2.6.0.dev11 → flamapy_fm-2.9.0.dev0}/flamapy/metamodels/fm_metamodel/transformations/glencoe_reader.py +0 -0
- {flamapy_fm-2.6.0.dev11 → flamapy_fm-2.9.0.dev0}/flamapy/metamodels/fm_metamodel/transformations/json_reader.py +0 -0
- {flamapy_fm-2.6.0.dev11 → flamapy_fm-2.9.0.dev0}/flamapy/metamodels/fm_metamodel/transformations/refactorings/__init__.py +0 -0
- {flamapy_fm-2.6.0.dev11 → flamapy_fm-2.9.0.dev0}/flamapy/metamodels/fm_metamodel/transformations/refactorings/commitment_feature.py +0 -0
- {flamapy_fm-2.6.0.dev11 → flamapy_fm-2.9.0.dev0}/flamapy/metamodels/fm_metamodel/transformations/refactorings/deletion_feature.py +0 -0
- {flamapy_fm-2.6.0.dev11 → flamapy_fm-2.9.0.dev0}/flamapy/metamodels/fm_metamodel/transformations/refactorings/feature_cardinality_refactoring.py +0 -0
- {flamapy_fm-2.6.0.dev11 → flamapy_fm-2.9.0.dev0}/flamapy/metamodels/fm_metamodel/transformations/refactorings/feature_cardinality_refactoring_paper.py +0 -0
- {flamapy_fm-2.6.0.dev11 → flamapy_fm-2.9.0.dev0}/flamapy/metamodels/fm_metamodel/transformations/refactorings/mutex_group_refactoring.py +0 -0
- {flamapy_fm-2.6.0.dev11 → flamapy_fm-2.9.0.dev0}/flamapy/metamodels/fm_metamodel/transformations/refactorings/or_mandatory_refactoring.py +0 -0
- {flamapy_fm-2.6.0.dev11 → flamapy_fm-2.9.0.dev0}/flamapy/metamodels/fm_metamodel/transformations/refactorings/pseudocomplex_constraint_refactoring.py +0 -0
- {flamapy_fm-2.6.0.dev11 → flamapy_fm-2.9.0.dev0}/flamapy/metamodels/fm_metamodel/transformations/refactorings/refactoring_exception.py +0 -0
- {flamapy_fm-2.6.0.dev11 → flamapy_fm-2.9.0.dev0}/flamapy/metamodels/fm_metamodel/transformations/refactorings/refactoring_interface.py +0 -0
- {flamapy_fm-2.6.0.dev11 → flamapy_fm-2.9.0.dev0}/flamapy/metamodels/fm_metamodel/transformations/refactorings/strictcomplex_constraint_refactoring.py +0 -0
- {flamapy_fm-2.6.0.dev11 → flamapy_fm-2.9.0.dev0}/flamapy/metamodels/fm_metamodel/transformations/refactorings/xor_mandatory_refactoring.py +0 -0
- {flamapy_fm-2.6.0.dev11 → flamapy_fm-2.9.0.dev0}/flamapy/metamodels/fm_metamodel/transformations/splot_writer.py +0 -0
- {flamapy_fm-2.6.0.dev11 → flamapy_fm-2.9.0.dev0}/flamapy/metamodels/fm_metamodel/transformations/xml_reader.py +0 -0
- {flamapy_fm-2.6.0.dev11 → flamapy_fm-2.9.0.dev0}/flamapy_fm.egg-info/dependency_links.txt +0 -0
- {flamapy_fm-2.6.0.dev11 → flamapy_fm-2.9.0.dev0}/flamapy_fm.egg-info/top_level.txt +0 -0
- {flamapy_fm-2.6.0.dev11 → flamapy_fm-2.9.0.dev0}/setup.cfg +0 -0
- {flamapy_fm-2.6.0.dev11 → flamapy_fm-2.9.0.dev0}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flamapy-fm
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.9.0.dev0
|
|
4
4
|
Summary: flamapy-fm is a plugin to Flamapy module
|
|
5
5
|
Author-email: Flamapy <flamapy@us.es>
|
|
6
6
|
License-Expression: GPL-3.0-or-later
|
|
@@ -8,7 +8,7 @@ Project-URL: Homepage, https://github.com/flamapy/fm_metamodel
|
|
|
8
8
|
Requires-Python: >=3.11
|
|
9
9
|
Description-Content-Type: text/markdown
|
|
10
10
|
License-File: LICENSE
|
|
11
|
-
Requires-Dist: flamapy-fw~=2.
|
|
11
|
+
Requires-Dist: flamapy-fw~=2.9.0.dev0
|
|
12
12
|
Requires-Dist: uvlparser~=2.5.0
|
|
13
13
|
Requires-Dist: afmparser~=1.0.3
|
|
14
14
|
Provides-Extra: dev
|
|
@@ -18,6 +18,10 @@ Requires-Dist: prospector; extra == "dev"
|
|
|
18
18
|
Requires-Dist: mypy; extra == "dev"
|
|
19
19
|
Requires-Dist: coverage; extra == "dev"
|
|
20
20
|
Requires-Dist: antlr4-tools; extra == "dev"
|
|
21
|
+
Provides-Extra: test
|
|
22
|
+
Requires-Dist: pytest; extra == "test"
|
|
23
|
+
Requires-Dist: flamapy-sat~=2.9.0.dev0; extra == "test"
|
|
24
|
+
Requires-Dist: flamapy-bdd~=2.9.0.dev0; extra == "test"
|
|
21
25
|
Dynamic: license-file
|
|
22
26
|
|
|
23
27
|
# flamapy-fm
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from flamapy.core.manifest import OperationCapability, PluginManifest
|
|
2
|
+
from flamapy.core.models.language_level import ALL_MINOR_LEVELS, LanguageLevel, MajorLevel
|
|
3
|
+
|
|
4
|
+
# The metamodel itself understands the full UVL language; its own (tree-level)
|
|
5
|
+
# operations run on any model. Costs are estimated milliseconds measured on 2026-11-08 on the
|
|
6
|
+
# betty calibration models (1% and 30% density, 1,000-10,000 features): both operations walk
|
|
7
|
+
# the tree once, core features in under 1 ms and atomic sets in 7 ms at 10,000 features.
|
|
8
|
+
MANIFEST = PluginManifest(
|
|
9
|
+
name='fm',
|
|
10
|
+
calibration='2026-11-08',
|
|
11
|
+
extension='fm',
|
|
12
|
+
supported_level=LanguageLevel(MajorLevel.TYPE, set(ALL_MINOR_LEVELS)),
|
|
13
|
+
operations={
|
|
14
|
+
# FMCoreFeatures reads the tree only (mandatory chains from the root) and ignores
|
|
15
|
+
# the cross-tree constraints: an estimate, never chosen by the planner unless the
|
|
16
|
+
# caller allows approximate results.
|
|
17
|
+
'core_features': OperationCapability(exact=False, cost=1),
|
|
18
|
+
# FMAtomicSets groups mandatory features with their parents and ignores the
|
|
19
|
+
# cross-tree constraints, which can merge sets: an estimate as well.
|
|
20
|
+
'atomic_sets': OperationCapability(
|
|
21
|
+
exact=False, cost_curve=((1000, 1), (2000, 2), (5000, 4), (10000, 7))),
|
|
22
|
+
# The estimated number of configurations is, by definition, an estimate (2 ms at
|
|
23
|
+
# 10,000 features).
|
|
24
|
+
'estimated_number_of_configurations': OperationCapability(exact=False, cost=2),
|
|
25
|
+
},
|
|
26
|
+
)
|
{flamapy_fm-2.6.0.dev11 → flamapy_fm-2.9.0.dev0}/flamapy/metamodels/fm_metamodel/models/__init__.py
RENAMED
|
@@ -10,9 +10,10 @@ from .feature_model import (
|
|
|
10
10
|
FeatureType,
|
|
11
11
|
AttributeType
|
|
12
12
|
)
|
|
13
|
-
from .clause_set import ClauseSet
|
|
13
|
+
from .clause_set import DEFAULT_CNF_METHOD, ClauseSet
|
|
14
14
|
|
|
15
15
|
__all__ = [
|
|
16
|
+
'DEFAULT_CNF_METHOD',
|
|
16
17
|
'Attribute',
|
|
17
18
|
'AttributeType',
|
|
18
19
|
'Cardinality',
|
|
@@ -10,9 +10,10 @@ their representation from this single encoding.
|
|
|
10
10
|
"""
|
|
11
11
|
import itertools
|
|
12
12
|
from dataclasses import dataclass, field
|
|
13
|
-
from typing import Any, Optional
|
|
13
|
+
from typing import Any, Callable, Optional
|
|
14
14
|
|
|
15
15
|
from flamapy.core.exceptions import FlamaException
|
|
16
|
+
from flamapy.core.models.ast import AST, ASTOperation, Node
|
|
16
17
|
|
|
17
18
|
from flamapy.metamodels.fm_metamodel.models.feature_model import (
|
|
18
19
|
FeatureModel,
|
|
@@ -22,6 +23,11 @@ from flamapy.metamodels.fm_metamodel.models.feature_model import (
|
|
|
22
23
|
)
|
|
23
24
|
|
|
24
25
|
|
|
26
|
+
# The encoding every CNF consumer uses unless told otherwise (FmToPysat, FmToDiagPysat, FmToSDD,
|
|
27
|
+
# FmToSharpSAT, FmToDDNNF): import it rather than repeating the literal.
|
|
28
|
+
DEFAULT_CNF_METHOD = 'auto'
|
|
29
|
+
|
|
30
|
+
|
|
25
31
|
@dataclass
|
|
26
32
|
class ClauseSet:
|
|
27
33
|
"""CNF encoding of a feature model as signed-integer clauses plus name/id maps."""
|
|
@@ -42,17 +48,90 @@ class ClauseSet:
|
|
|
42
48
|
|
|
43
49
|
@classmethod
|
|
44
50
|
def from_feature_model(
|
|
45
|
-
cls, feature_model: FeatureModel, cnf_method: str =
|
|
51
|
+
cls, feature_model: FeatureModel, cnf_method: str = DEFAULT_CNF_METHOD
|
|
46
52
|
) -> 'ClauseSet':
|
|
47
53
|
"""Encode ``feature_model`` into CNF.
|
|
48
54
|
|
|
49
55
|
``cnf_method`` controls only how cross-tree constraints are turned into clauses:
|
|
50
|
-
``'distributive'``
|
|
51
|
-
|
|
56
|
+
``'distributive'`` expands them directly, ``'tseytin'`` introduces auxiliary variables
|
|
57
|
+
for a compact, model-count-preserving encoding, and ``'auto'`` (the default since 2.9)
|
|
58
|
+
decides per constraint: it expands a constraint directly unless the expansion would be
|
|
59
|
+
larger than its Tseytin encoding (see `distributive_clause_count`), so only constraints
|
|
60
|
+
that blow up (e.g. disjunctions of conjunctions, wide equivalences) get auxiliary
|
|
61
|
+
variables. On the SoSyM benchmark corpus ``'auto'`` encodes 1,409 of 1,538 models exactly
|
|
62
|
+
as ``'distributive'`` does; on the other 129 it produces fewer clauses (never more than
|
|
63
|
+
7,000 where the expansion reaches 51,000), encodes 2.5x faster and gives the same
|
|
64
|
+
results for every operation.
|
|
52
65
|
"""
|
|
66
|
+
if cnf_method not in CNF_METHODS:
|
|
67
|
+
raise FlamaException(f'Unknown cnf_method {cnf_method!r}; expected one of '
|
|
68
|
+
f'{", ".join(CNF_METHODS)}.')
|
|
53
69
|
return _Encoder(cnf_method).encode(feature_model)
|
|
54
70
|
|
|
55
71
|
|
|
72
|
+
CNF_METHODS = ('distributive', 'tseytin', 'auto')
|
|
73
|
+
BINARY = 2
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# Clause counts of (P op Q) and of its negation, from the counts of P and Q in both polarities,
|
|
77
|
+
# following flamapy's rewriting (simplify_formula): P => Q as !P | Q, P excludes Q as !P | !Q,
|
|
78
|
+
# P <=> Q as (P => Q) & (Q => P), P xor Q as (P & !Q) | (!P & Q). A conjunction adds the clauses
|
|
79
|
+
# of its operands, a disjunction multiplies them.
|
|
80
|
+
_COUNT_RULES: dict[ASTOperation, Callable[[int, int, int, int], tuple[int, int]]] = {
|
|
81
|
+
ASTOperation.AND: lambda ap, an, bp, bn: (ap + bp, an * bn),
|
|
82
|
+
ASTOperation.OR: lambda ap, an, bp, bn: (ap * bp, an + bn),
|
|
83
|
+
ASTOperation.IMPLIES: lambda ap, an, bp, bn: (an * bp, ap + bn),
|
|
84
|
+
ASTOperation.REQUIRES: lambda ap, an, bp, bn: (an * bp, ap + bn),
|
|
85
|
+
ASTOperation.EXCLUDES: lambda ap, an, bp, bn: (an * bn, ap + bp),
|
|
86
|
+
ASTOperation.EQUIVALENCE: lambda ap, an, bp, bn: (an * bp + ap * bn, ap * bp + an * bn),
|
|
87
|
+
ASTOperation.XOR: lambda ap, an, bp, bn: ((ap + bn) * (an + bp), an * bp + ap * bn),
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def distributive_clause_count(ast: AST) -> Optional[int]:
|
|
92
|
+
"""Number of clauses the distributive CNF expansion of `ast` produces, computed without
|
|
93
|
+
building it (duplicates and subsumed clauses are not removed, as in the expansion itself).
|
|
94
|
+
|
|
95
|
+
Each node is counted for both polarities (see _COUNT_RULES), in one iterative pass, so deep
|
|
96
|
+
constraints do not hit the recursion limit. Returns None for non-propositional operators.
|
|
97
|
+
"""
|
|
98
|
+
pos: dict[int, int] = {}
|
|
99
|
+
neg: dict[int, int] = {}
|
|
100
|
+
stack: list[tuple[Node, bool]] = [(ast.root, False)]
|
|
101
|
+
while stack:
|
|
102
|
+
node, expanded = stack.pop()
|
|
103
|
+
if node.is_term():
|
|
104
|
+
pos[id(node)] = neg[id(node)] = 1
|
|
105
|
+
continue
|
|
106
|
+
children = [child for child in (node.left, node.right) if child is not None]
|
|
107
|
+
if not expanded:
|
|
108
|
+
stack.append((node, True))
|
|
109
|
+
stack.extend((child, False) for child in children)
|
|
110
|
+
continue
|
|
111
|
+
if any(id(child) not in pos for child in children):
|
|
112
|
+
return None
|
|
113
|
+
if node.data == ASTOperation.NOT:
|
|
114
|
+
pos[id(node)], neg[id(node)] = neg[id(children[0])], pos[id(children[0])]
|
|
115
|
+
elif node.data in _COUNT_RULES and len(children) == BINARY:
|
|
116
|
+
left, right = children
|
|
117
|
+
pos[id(node)], neg[id(node)] = _COUNT_RULES[node.data](
|
|
118
|
+
pos[id(left)], neg[id(left)], pos[id(right)], neg[id(right)])
|
|
119
|
+
else:
|
|
120
|
+
return None
|
|
121
|
+
return pos[id(ast.root)]
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def tseytin_clause_estimate(ast: AST) -> int:
|
|
125
|
+
"""Approximate size of the Tseytin encoding: about three clauses per operator, plus one."""
|
|
126
|
+
return 3 * sum(1 for _ in ast.get_operators()) + 1
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def use_tseytin(ast: AST) -> bool:
|
|
130
|
+
"""The `auto` rule: Tseytin when the distributive expansion would be larger."""
|
|
131
|
+
count = distributive_clause_count(ast)
|
|
132
|
+
return count is not None and count > tseytin_clause_estimate(ast)
|
|
133
|
+
|
|
134
|
+
|
|
56
135
|
class _Encoder:
|
|
57
136
|
"""Ported ~1:1 from the SAT plugin's FmToPysat so the produced clauses are identical."""
|
|
58
137
|
|
|
@@ -145,15 +224,9 @@ class _Encoder:
|
|
|
145
224
|
else:
|
|
146
225
|
cnf.append(self._get_variable(feat.name))
|
|
147
226
|
clauses.append(cnf)
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
for feat in relation.children:
|
|
152
|
-
if feat in combination:
|
|
153
|
-
cnf.append(-self._get_variable(feat.name))
|
|
154
|
-
else:
|
|
155
|
-
cnf.append(self._get_variable(feat.name))
|
|
156
|
-
clauses.append(cnf)
|
|
227
|
+
# Every selected child implies the parent: one binary clause per child.
|
|
228
|
+
for feat in relation.children:
|
|
229
|
+
clauses.append([-self._get_variable(feat.name), parent])
|
|
157
230
|
return clauses
|
|
158
231
|
|
|
159
232
|
def _add_relation(self, relation: Relation) -> None:
|
|
@@ -180,7 +253,10 @@ class _Encoder:
|
|
|
180
253
|
return aux_map
|
|
181
254
|
|
|
182
255
|
def _add_constraint(self, ctc: Constraint) -> None:
|
|
183
|
-
|
|
256
|
+
method = self.cnf_method
|
|
257
|
+
if method == 'auto':
|
|
258
|
+
method = 'tseytin' if use_tseytin(ctc.ast) else 'distributive'
|
|
259
|
+
if method == 'tseytin':
|
|
184
260
|
clauses, aux_names = ctc.ast.get_clauses_with_aux(method='tseytin')
|
|
185
261
|
aux_map = self._allocate_auxiliary(aux_names)
|
|
186
262
|
else:
|
|
@@ -118,7 +118,7 @@ class Cardinality:
|
|
|
118
118
|
@total_ordering
|
|
119
119
|
class Feature(VariabilityElement):
|
|
120
120
|
|
|
121
|
-
def __init__( # noqa: PLR0913
|
|
121
|
+
def __init__( # noqa: PLR0913, PLR0917
|
|
122
122
|
self,
|
|
123
123
|
name: str,
|
|
124
124
|
relations: Optional[list["Relation"]] = None,
|
|
@@ -398,6 +398,23 @@ class FeatureModel(VariabilityModel):
|
|
|
398
398
|
self.ctcs = [] if constraints is None else constraints
|
|
399
399
|
self.imports: dict[str, FeatureModel] = {} # namespace -> FeatureModel
|
|
400
400
|
self.alias_namespace: dict[str, str] = {} # alias -> namespace
|
|
401
|
+
self._canonical_hash: Optional[str] = None
|
|
402
|
+
|
|
403
|
+
def canonical_hash(self) -> Optional[str]:
|
|
404
|
+
"""sha256 of the model's canonical UVL serialization, memoized.
|
|
405
|
+
|
|
406
|
+
Mutating the model does not invalidate the memo — hash before editing, or build a
|
|
407
|
+
fresh FeatureModel. (UVLWriter is imported lazily: transformations import models.)
|
|
408
|
+
"""
|
|
409
|
+
if self._canonical_hash is None:
|
|
410
|
+
import hashlib # noqa: PLC0415
|
|
411
|
+
# Lazy: transformations import models, so this import cannot be top-level.
|
|
412
|
+
from flamapy.metamodels.fm_metamodel.transformations.uvl_writer import ( # noqa: PLC0415 pylint: disable=import-outside-toplevel,cyclic-import
|
|
413
|
+
UVLWriter,
|
|
414
|
+
)
|
|
415
|
+
uvl = UVLWriter(path=None, source_model=self).transform() # type: ignore[arg-type]
|
|
416
|
+
self._canonical_hash = hashlib.sha256(uvl.encode('utf-8')).hexdigest()
|
|
417
|
+
return self._canonical_hash
|
|
401
418
|
|
|
402
419
|
def get_relations(self, feature: Optional["Feature"] = None) -> list["Relation"]:
|
|
403
420
|
if self.root is None or self.root.is_empty():
|
|
@@ -1,25 +1,18 @@
|
|
|
1
|
-
from typing import
|
|
1
|
+
from typing import cast
|
|
2
2
|
|
|
3
3
|
from flamapy.core.models import VariabilityModel
|
|
4
4
|
from flamapy.core.operations.atomic_sets import AtomicSets
|
|
5
|
-
from flamapy.core.operations.descriptor import OperationDescriptor
|
|
6
5
|
from flamapy.metamodels.fm_metamodel.models import FeatureModel, Feature
|
|
7
6
|
|
|
8
7
|
|
|
9
|
-
def _atomic_sets_result(result: Any) -> Any:
|
|
10
|
-
return [[feature.name for feature in atomic_set] for atomic_set in result]
|
|
11
|
-
|
|
12
|
-
|
|
13
8
|
class FMAtomicSets(AtomicSets):
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
name='atomic_sets', operation='FMAtomicSets', result_adapter=_atomic_sets_result
|
|
22
|
-
)
|
|
9
|
+
"""The atomic sets read from the feature tree: the root with its chain of mandatory
|
|
10
|
+
descendants, and every other feature with its own chain. Cross-tree constraints, which
|
|
11
|
+
can merge sets, are ignored (the facade descriptor is the interface's; the exact sets are
|
|
12
|
+
computed by the solver-based implementations)."""
|
|
13
|
+
|
|
14
|
+
# Ignores the cross-tree constraints: reported in the OperationResult provenance envelope.
|
|
15
|
+
exact = False
|
|
23
16
|
|
|
24
17
|
def __init__(self) -> None:
|
|
25
18
|
self.result: list[set[Feature]] = []
|
|
@@ -18,6 +18,10 @@ class FMCoreFeatures(CoreFeatures):
|
|
|
18
18
|
false-optional features (e.g., for 'requires' constraints).
|
|
19
19
|
"""
|
|
20
20
|
|
|
21
|
+
# Reads the tree only, so the result can miss core features that cross-tree constraints
|
|
22
|
+
# force: reported in the OperationResult provenance envelope.
|
|
23
|
+
exact = False
|
|
24
|
+
|
|
21
25
|
def __init__(self) -> None:
|
|
22
26
|
self.result: list[Feature] = []
|
|
23
27
|
|
|
@@ -16,6 +16,8 @@ class FMEstimatedConfigurationsNumber(EstimatedConfigurationsNumber):
|
|
|
16
16
|
and thus, the number is an upper limit of the real number of products.
|
|
17
17
|
"""
|
|
18
18
|
|
|
19
|
+
exact = False # reported in the OperationResult provenance envelope
|
|
20
|
+
|
|
19
21
|
facade = OperationDescriptor(
|
|
20
22
|
doc=(
|
|
21
23
|
'This is an estimate of the total number of different products that can be\n'
|
|
@@ -1,37 +1,19 @@
|
|
|
1
|
-
from enum import Enum, auto
|
|
2
|
-
from dataclasses import dataclass, field
|
|
3
1
|
from typing import cast
|
|
4
2
|
|
|
5
3
|
from flamapy.core.models import VariabilityModel, ASTOperation
|
|
4
|
+
# LanguageLevel/MajorLevel/MinorLevel moved to flamapy.core.models.language_level (they now
|
|
5
|
+
# also describe plugin capability manifests); re-exported here for compatibility until 3.0.
|
|
6
|
+
from flamapy.core.models.language_level import (
|
|
7
|
+
LanguageLevel,
|
|
8
|
+
MajorLevel,
|
|
9
|
+
MinorLevel,
|
|
10
|
+
)
|
|
6
11
|
from flamapy.core.operations import Operation
|
|
7
12
|
from flamapy.core.operations.descriptor import OperationDescriptor
|
|
8
13
|
|
|
9
14
|
from flamapy.metamodels.fm_metamodel.models import FeatureModel, Constraint
|
|
10
15
|
|
|
11
16
|
|
|
12
|
-
class MajorLevel(Enum):
|
|
13
|
-
BOOLEAN = 1
|
|
14
|
-
ARITHMETIC = 2
|
|
15
|
-
TYPE = 3
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class MinorLevel(Enum):
|
|
19
|
-
STRING_CONSTRAINTS = auto()
|
|
20
|
-
FEATURE_CARDINALITY = auto()
|
|
21
|
-
AGGREGATE_FUNCTION = auto()
|
|
22
|
-
GROUP_CARDINALITY = auto()
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
@dataclass(order=True, frozen=True)
|
|
26
|
-
class LanguageLevel:
|
|
27
|
-
major: MajorLevel
|
|
28
|
-
minors: set[MinorLevel] = field(default_factory=set)
|
|
29
|
-
|
|
30
|
-
def __str__(self) -> str:
|
|
31
|
-
minors_str = ', '.join(ext.name for ext in sorted(self.minors, key=lambda x: x.name))
|
|
32
|
-
return f'Major: {self.major.name}, Minor: {{{minors_str}}}'
|
|
33
|
-
|
|
34
|
-
|
|
35
17
|
class FMLanguageLevel(Operation):
|
|
36
18
|
"""Operation to calculate the maximum language level of a feature model."""
|
|
37
19
|
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Work around a lexer slowdown in uvlparser <= 2.5.0.
|
|
2
|
+
|
|
3
|
+
The generated UVL lexer's NEWLINE rule starts with the semantic predicate
|
|
4
|
+
``{self.atStartOfInput()}?``. ANTLR never caches the lexer's DFA start state when a predicate
|
|
5
|
+
is reachable from it, so every token recomputes the full ATN closure (~6x slower parsing).
|
|
6
|
+
|
|
7
|
+
The predicate is ``line == 1 and column == 0``, so it can only hold on the very first token.
|
|
8
|
+
The simulator below bypasses the start-state cache on that token and caches it everywhere
|
|
9
|
+
else, where the predicate is false and the closure does not depend on the position. Lexing
|
|
10
|
+
results are identical, including the rejection of an indented first line.
|
|
11
|
+
|
|
12
|
+
Once uvlparser ships a lexer without the predicate this becomes a no-op and can be removed.
|
|
13
|
+
"""
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from antlr4 import InputStream
|
|
17
|
+
from antlr4.PredictionContext import PredictionContextCache
|
|
18
|
+
from antlr4.atn.LexerATNSimulator import LexerATNSimulator
|
|
19
|
+
from antlr4.dfa.DFA import DFA
|
|
20
|
+
from uvl.UVLCustomLexer import UVLCustomLexer
|
|
21
|
+
from uvl.UVLPythonLexer import UVLPythonLexer
|
|
22
|
+
|
|
23
|
+
NEEDS_WORKAROUND = hasattr(UVLPythonLexer, 'NEWLINE_sempred')
|
|
24
|
+
|
|
25
|
+
# A private DFA cache for the patched lexers. ANTLR shares UVLPythonLexer.decisionsToDFA across
|
|
26
|
+
# all lexer instances; filling its start state there would leak into unpatched UVL lexers,
|
|
27
|
+
# which would then use it on the first token and skip the predicate.
|
|
28
|
+
_DECISIONS_TO_DFA = [DFA(state, index)
|
|
29
|
+
for index, state in enumerate(UVLPythonLexer.atn.decisionToState)]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class _StartStateCachingSimulator(LexerATNSimulator):
|
|
33
|
+
|
|
34
|
+
# Parameter names follow LexerATNSimulator (pylint: arguments-renamed otherwise).
|
|
35
|
+
# pylint: disable=redefined-builtin
|
|
36
|
+
def match(self, input: InputStream, mode: int) -> int:
|
|
37
|
+
if self.line == 1 and self.column == 0:
|
|
38
|
+
# The only position where atStartOfInput() holds: neither read nor fill the cache.
|
|
39
|
+
self.mode = mode
|
|
40
|
+
mark = input.mark()
|
|
41
|
+
try:
|
|
42
|
+
self.startIndex = input.index
|
|
43
|
+
self.prevAccept.reset()
|
|
44
|
+
return int(self.execATN(input, self._start_state(input)))
|
|
45
|
+
finally:
|
|
46
|
+
input.release(mark)
|
|
47
|
+
return int(super().match(input, mode))
|
|
48
|
+
|
|
49
|
+
def matchATN(self, input: InputStream) -> int:
|
|
50
|
+
# Past the first token the predicate is false, so the start state is safe to cache.
|
|
51
|
+
start = self._start_state(input)
|
|
52
|
+
self.decisionToDFA[self.mode].s0 = start
|
|
53
|
+
return int(self.execATN(input, start))
|
|
54
|
+
|
|
55
|
+
def _start_state(self, input: InputStream) -> Any:
|
|
56
|
+
closure = self.computeStartState(input, self.atn.modeToStartState[self.mode])
|
|
57
|
+
closure.hasSemanticContext = False
|
|
58
|
+
return self.addDFAState(closure)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def uvl_lexer(input_stream: InputStream) -> UVLCustomLexer:
|
|
62
|
+
"""A UVL lexer, with the start-state cache workaround when the installed parser needs it."""
|
|
63
|
+
lexer = UVLCustomLexer(input_stream)
|
|
64
|
+
if NEEDS_WORKAROUND:
|
|
65
|
+
lexer._interp = _StartStateCachingSimulator( # pylint: disable=protected-access
|
|
66
|
+
lexer, lexer.atn, _DECISIONS_TO_DFA, PredictionContextCache())
|
|
67
|
+
return lexer
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import os
|
|
2
2
|
|
|
3
|
-
from typing import Optional
|
|
3
|
+
from typing import Any, Optional
|
|
4
4
|
|
|
5
|
-
from
|
|
6
|
-
from
|
|
5
|
+
from antlr4 import CommonTokenStream, FileStream
|
|
6
|
+
from antlr4.error.ErrorListener import ErrorListener
|
|
7
|
+
from afmparser import AFMLexer, AFMParser
|
|
7
8
|
|
|
8
9
|
from flamapy.core.exceptions import FlamaException
|
|
9
10
|
from flamapy.core.transformations import TextToModel
|
|
@@ -19,7 +20,32 @@ from flamapy.metamodels.fm_metamodel.models import (
|
|
|
19
20
|
)
|
|
20
21
|
|
|
21
22
|
|
|
23
|
+
class _RaisingErrorListener(ErrorListener):
|
|
24
|
+
"""Turn syntax errors into exceptions: the generated parser recovers from them and the
|
|
25
|
+
reader would otherwise fail later on a half-built parse tree."""
|
|
26
|
+
|
|
27
|
+
def syntaxError( # noqa: PLR0913, PLR0917 (ANTLR's signature)
|
|
28
|
+
self, recognizer: Any, offendingSymbol: Any, line: int, column: int, msg: str, e: Any
|
|
29
|
+
) -> None:
|
|
30
|
+
raise FlamaException(f"AFM syntax error at line {line}:{column}: {msg}")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get_tree(path: str) -> AFMParser.Feature_modelContext:
|
|
34
|
+
"""Parse an AFM file (UTF-8), raising FlamaException on the first syntax error."""
|
|
35
|
+
lexer = AFMLexer(FileStream(path, encoding="utf-8"))
|
|
36
|
+
lexer.removeErrorListeners()
|
|
37
|
+
lexer.addErrorListener(_RaisingErrorListener())
|
|
38
|
+
parser = AFMParser(CommonTokenStream(lexer))
|
|
39
|
+
parser.removeErrorListeners()
|
|
40
|
+
parser.addErrorListener(_RaisingErrorListener())
|
|
41
|
+
return parser.feature_model()
|
|
42
|
+
|
|
43
|
+
|
|
22
44
|
class AFMReader(TextToModel):
|
|
45
|
+
"""Read the AFM text format of FaMa. Relationships, attributes (Integer ranges and
|
|
46
|
+
discrete domains) and the logical constraints are read; constraints with relational or
|
|
47
|
+
arithmetic operators are not supported."""
|
|
48
|
+
|
|
23
49
|
@staticmethod
|
|
24
50
|
def get_source_extension() -> str:
|
|
25
51
|
return "afm"
|
|
@@ -141,7 +167,8 @@ class AFMReader(TextToModel):
|
|
|
141
167
|
if range_domain_node is not None:
|
|
142
168
|
range_list = []
|
|
143
169
|
for domain_range in range_domain_node.domain_range():
|
|
144
|
-
range_list.append(Range(domain_range.INT()[0]
|
|
170
|
+
range_list.append(Range(int(domain_range.INT()[0].getText()),
|
|
171
|
+
int(domain_range.INT()[1].getText())))
|
|
145
172
|
domain = Domain(range_list, None)
|
|
146
173
|
|
|
147
174
|
default_value = attribute_spec.attribute_default_value().value_spec().getText()
|
|
@@ -203,6 +230,7 @@ class AFMReader(TextToModel):
|
|
|
203
230
|
"OR": ASTOperation.OR,
|
|
204
231
|
"AND": ASTOperation.AND,
|
|
205
232
|
"IFF": ASTOperation.EQUIVALENCE,
|
|
233
|
+
"IMPLIES": ASTOperation.IMPLIES,
|
|
206
234
|
}
|
|
207
235
|
|
|
208
236
|
if expression.__class__ in binary_operation_types:
|
|
@@ -219,8 +247,10 @@ class AFMReader(TextToModel):
|
|
|
219
247
|
result.right = self.build_ast_node(expression.expression()[1], prefix)
|
|
220
248
|
|
|
221
249
|
if isinstance(expression, AFMParser.NotExpContext):
|
|
250
|
+
# the operand of a unary operator is the left child (as the other readers and
|
|
251
|
+
# the AST simplification expect)
|
|
222
252
|
result = Node(ASTOperation.NOT)
|
|
223
|
-
result.
|
|
253
|
+
result.left = self.build_ast_node(expression.expression(), prefix)
|
|
224
254
|
|
|
225
255
|
if isinstance(expression, AFMParser.ParenthesisExpContext):
|
|
226
256
|
result = self.build_ast_node(expression.expression(), prefix)
|