vnnlib-test-solver 2.0.0__py3-none-any.whl

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,119 @@
1
+ """Rule matching: which configured answer applies to a query file.
2
+
3
+ The ``[[rules]]`` array is what makes this a test solver rather than a solver.
4
+ An answer is chosen by looking the query file up in the configuration, never by
5
+ examining anything the file says, which is the whole of the arrangement: one
6
+ installed binary, three configuration files, three answers.
7
+
8
+ **First match wins**, in the order the rules are written. The alternative - the
9
+ most specific pattern wins - needs a definition of specificity for two arbitrary
10
+ globs, and every such definition is a surprise waiting for whoever writes the
11
+ second rule. Written order is a rule a reader can apply by eye.
12
+
13
+ **No matching rule is an error, not a default.** Silence in a configuration file
14
+ is not a claim, and a conformance baseline that invents an answer is worse than
15
+ one that refuses to give it.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import fnmatch
21
+ from collections.abc import Mapping, Sequence
22
+ from dataclasses import dataclass
23
+ from pathlib import Path
24
+ from typing import Any, Optional
25
+
26
+ from .config import Config
27
+ from .errors import ConfigError
28
+
29
+
30
+ @dataclass(frozen=True)
31
+ class Rule:
32
+ """One configured answer, built from a validated ``[[rules]]`` entry.
33
+
34
+ Every key the schema permits is read here rather than at the point it is
35
+ first used, so this record and the validated schema stay one list that can
36
+ be compared by eye.
37
+
38
+ ``model_element_types`` is what the configuration claims the supplied model
39
+ files expose, keyed by variable name. It is the only source this package
40
+ has for section 5.3.2's second error condition, since nothing here opens an
41
+ ONNX file.
42
+ """
43
+
44
+ match: str
45
+ result: str
46
+ assignments: Mapping[str, Sequence[float]]
47
+ model_element_types: Mapping[str, str]
48
+ delay_seconds: int
49
+ stderr: Optional[str]
50
+ exit_code: Optional[int]
51
+ raw_stdout: Optional[str]
52
+ crash: Optional[bool]
53
+ """``None`` when the rule does not mention it, which is not the same as
54
+ ``False``.
55
+
56
+ An explicit ``crash = false`` is a rule saying *do not* crash, and it has to
57
+ be able to override a ``[injection]`` table that says otherwise. Storing
58
+ both as ``False`` made those two indistinguishable and silently lost the
59
+ override - found in review, not by a test.
60
+ """
61
+
62
+
63
+ def select(config: Config, query: Path) -> Rule:
64
+ """Return the first configured rule matching ``query``.
65
+
66
+ Raises:
67
+ ConfigError: no rule matches, so the configuration has no answer to give.
68
+ """
69
+ for entry in config.rules:
70
+ if matches(entry["match"], query):
71
+ return _rule(entry)
72
+ raise ConfigError(_no_match_message(config, query))
73
+
74
+
75
+ def matches(pattern: str, query: Path) -> bool:
76
+ """Compare one pattern against the query file's name.
77
+
78
+ ``fnmatchcase`` rather than ``fnmatch``: the latter folds case through
79
+ ``os.path.normcase``, which does nothing on POSIX and lower-cases on
80
+ Windows. The same configuration and the same query would then answer
81
+ differently on different machines, and a conformance baseline that is
82
+ platform-dependent is not a baseline.
83
+
84
+ The file's name rather than the path it was given by, so a configuration
85
+ keeps working from whichever directory a caller happens to run in.
86
+ """
87
+ return fnmatch.fnmatchcase(query.name, pattern)
88
+
89
+
90
+ def _no_match_message(config: Config, query: Path) -> str:
91
+ """Say what was matched, against what, so a near miss is diagnosable.
92
+
93
+ A pattern written against a path rather than a file name fails silently
94
+ otherwise, and looks like the rule being ignored.
95
+ """
96
+ if not config.rules:
97
+ return (
98
+ f"no rule matches query file {query.name!r}: {config.source.path} "
99
+ f"defines no rules at all, so it has no answer to give"
100
+ )
101
+ patterns = ", ".join(repr(entry["match"]) for entry in config.rules)
102
+ return (
103
+ f"no rule matches query file {query.name!r}; a pattern is compared against "
104
+ f"the file name alone, and {config.source.path} defines {patterns}"
105
+ )
106
+
107
+
108
+ def _rule(entry: Mapping[str, Any]) -> Rule:
109
+ return Rule(
110
+ match=entry["match"],
111
+ result=entry["result"],
112
+ assignments=entry.get("assignments", {}),
113
+ model_element_types=entry.get("model_element_types", {}),
114
+ delay_seconds=entry.get("delay_seconds", 0),
115
+ stderr=entry.get("stderr"),
116
+ exit_code=entry.get("exit_code"),
117
+ raw_stdout=entry.get("raw_stdout"),
118
+ crash=entry.get("crash"),
119
+ )
@@ -0,0 +1,79 @@
1
+ """Validation targets transcribed from the standard.
2
+
3
+ Theory identifier sets are section 4.1; element type names are the ONNX Set 1 names
4
+ declarable in a query, plus the non-ONNX ``real``. Sets are frozen because nothing may
5
+ extend them at runtime, and every message that names one sorts it first: unordered
6
+ iteration in emitted output would break determinism.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ HIDDEN_NODE_THEORIES = frozenset({"NH", "H"})
12
+ MULTIPLE_INPUT_OUTPUT_THEORIES = frozenset({"SIO", "MIO"})
13
+ MULTIPLE_NETWORK_THEORIES = frozenset({"SNET", "MNET", "MENET", "MINET"})
14
+ MULTIPLE_NODE_COMPARISON_THEORIES = frozenset({"SNC", "MNC"})
15
+ ARITHMETIC_COMPLEXITY_THEORIES = frozenset({"BND", "OUTC", "LIN", "POLY"})
16
+
17
+ REAL = "real"
18
+ """The one element type the standard defines that ONNX does not.
19
+
20
+ Named for the reason TIMED_OUT and SAT are: section 5.3.2 exempts a
21
+ declaration written in this type from two of its three error conditions **by
22
+ name**, so this package now acts on the value rather than only listing it
23
+ among the others. A literal repeated in three places is free to drift away
24
+ from the set it is supposed to belong to.
25
+
26
+ Writing a declaration in this type is how section 3.5 says a user consents to
27
+ analysis that may be unsound under floating point, which is why the exemption
28
+ exists at all: there is nothing left to warn them about.
29
+ """
30
+
31
+ ELEMENT_TYPES = frozenset(
32
+ {
33
+ REAL,
34
+ "float16",
35
+ "float32",
36
+ "float64",
37
+ "bfloat16",
38
+ "float8e4m3fn",
39
+ "float8e5m2",
40
+ "float8e4m3fnuz",
41
+ "float8e5m2fnuz",
42
+ "float4e2m1",
43
+ "int8",
44
+ "int16",
45
+ "int32",
46
+ "int64",
47
+ "uint8",
48
+ "uint16",
49
+ "uint32",
50
+ "uint64",
51
+ "complex64",
52
+ "complex128",
53
+ "bool",
54
+ }
55
+ )
56
+
57
+ TIMED_OUT = "timed-out"
58
+ """Section 5.3.1's answer for a solver that ran out of the time it was given.
59
+
60
+ Named because it is the one result this package produces without being asked
61
+ for it, when a rule's delay outlasts the caller's timeout. The other three are
62
+ only ever quoted from configuration.
63
+ """
64
+
65
+ SAT = "sat"
66
+ """Section 5.3.1's answer for a query the solver found satisfiable.
67
+
68
+ Named for the same reason TIMED_OUT is: this package acts on it rather than
69
+ only quoting it back, because section 5.3.1 attaches the assignment block to
70
+ this result and to no other. A literal in two places is a literal that can
71
+ drift apart from the set below.
72
+ """
73
+
74
+ RESULTS = (TIMED_OUT, "unknown", "unsat", SAT)
75
+
76
+
77
+ def as_list(values: frozenset[str]) -> str:
78
+ """Render a set for an error message in a stable order."""
79
+ return ", ".join(sorted(values))
@@ -0,0 +1,151 @@
1
+ """The supports command: capability responses, per standard section 5.4.
2
+
3
+ Eleven mandatory capabilities in four distinct response shapes: a pair of version
4
+ strings, a newline-separated list, one line per ONNX operator, and a boolean
5
+ literal. Only two of the eleven are booleans, so each shape is rendered on its
6
+ own terms rather than through one generic path.
7
+
8
+ Every answer comes from configuration. A capability the configuration does not
9
+ report is an error rather than a guess: a conformance baseline that invents an
10
+ answer is worse than one that refuses to give it.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from collections.abc import Iterable, Mapping, Sequence
16
+ from typing import Any
17
+
18
+ from .config import Config
19
+ from .errors import ConfigError
20
+
21
+ OPSET_VERSIONS = "--onnx-opset-versions"
22
+ ELEMENT_TYPES = "--onnx-element-types"
23
+ OPERATORS = "--onnx-operators"
24
+ VNNLIB_VERSIONS = "--vnnlib-versions"
25
+ HIDDEN_NODE = "--hidden-node-theories"
26
+ MULTIPLE_INPUT_OUTPUT = "--multiple-input-output-theories"
27
+ MULTIPLE_NETWORK = "--multiple-network-theories"
28
+ MULTIPLE_NODE_COMPARISON = "--multiple-node-comparison-theories"
29
+ ARITHMETIC_COMPLEXITY = "--arithmetic-complexity-theories"
30
+ OPTIMISED_DISJUNCTIVE_REASONING = "--optimised-disjunctive-reasoning"
31
+ SERIALISE_ASSIGNMENTS = "--serialise-assignments"
32
+
33
+ CAPABILITIES = (
34
+ OPSET_VERSIONS,
35
+ ELEMENT_TYPES,
36
+ OPERATORS,
37
+ VNNLIB_VERSIONS,
38
+ HIDDEN_NODE,
39
+ MULTIPLE_INPUT_OUTPUT,
40
+ MULTIPLE_NETWORK,
41
+ MULTIPLE_NODE_COMPARISON,
42
+ ARITHMETIC_COMPLEXITY,
43
+ OPTIMISED_DISJUNCTIVE_REASONING,
44
+ SERIALISE_ASSIGNMENTS,
45
+ )
46
+ """Every capability, in the order section 5.4 introduces them.
47
+
48
+ A tuple rather than a set: this order reaches stderr in the message naming the
49
+ legal capabilities, and unordered iteration in emitted output is not permitted.
50
+ """
51
+
52
+ MULTIPLE_INPUT_OUTPUT_ALIAS = "--multiple-inputs-output-theories"
53
+ """Section 5.4.2 spells the flag one way in its heading and another, with an
54
+ extra 's', in its own example. Both are accepted; the heading spelling is the
55
+ documented one."""
56
+
57
+ VERSION_PAIR_CAPABILITIES = (OPSET_VERSIONS, VNNLIB_VERSIONS)
58
+ LIST_CAPABILITIES = (
59
+ ELEMENT_TYPES,
60
+ HIDDEN_NODE,
61
+ MULTIPLE_INPUT_OUTPUT,
62
+ MULTIPLE_NETWORK,
63
+ MULTIPLE_NODE_COMPARISON,
64
+ ARITHMETIC_COMPLEXITY,
65
+ )
66
+ BOOLEAN_CAPABILITIES = (OPTIMISED_DISJUNCTIVE_REASONING, SERIALISE_ASSIGNMENTS)
67
+
68
+
69
+ def config_key(capability: str) -> str:
70
+ """The [capabilities] key that answers a capability flag.
71
+
72
+ Every flag in section 5.4 is its configuration key with the option dashes
73
+ removed, so the published flag and the documented key cannot drift apart.
74
+ """
75
+ return capability[2:]
76
+
77
+
78
+ def response_shape(capability: str) -> str:
79
+ """Describe a capability's response shape, for the command's own help."""
80
+ if capability in VERSION_PAIR_CAPABILITIES:
81
+ return "two version strings, minimum then maximum"
82
+ if capability in BOOLEAN_CAPABILITIES:
83
+ return "true or false"
84
+ if capability == OPERATORS:
85
+ return "one operator per line, with the element types it supports"
86
+ if capability == ELEMENT_TYPES:
87
+ return "one element type per line"
88
+ return "one theory identifier per line"
89
+
90
+
91
+ def render(capability: str, config: Config) -> str:
92
+ """Return the exact stdout text answering one capability.
93
+
94
+ The trailing newline is part of the answer. A capability configured as an
95
+ empty list renders as the empty string: supporting nothing is an answer, and
96
+ a blank line would not be one.
97
+ """
98
+ reported = _reported(capability, config)
99
+
100
+ if capability in LIST_CAPABILITIES:
101
+ return _lines(reported)
102
+
103
+ if capability in VERSION_PAIR_CAPABILITIES:
104
+ # Two lines, minimum then maximum. Configuration validation has already
105
+ # rejected any count other than two, so the shape is guaranteed here.
106
+ return _lines(reported)
107
+
108
+ if capability == OPERATORS:
109
+ return _operator_lines(reported)
110
+
111
+ if capability in BOOLEAN_CAPABILITIES:
112
+ # The specification's literals, not Python's: bool renders as True/False.
113
+ return "true\n" if reported else "false\n"
114
+
115
+ # Unreachable through the command line, where the flags are built from
116
+ # CAPABILITIES and a test asserts the four shapes above partition it. Kept so
117
+ # that adding a twelfth capability fails loudly rather than silently.
118
+ raise ConfigError(f"{capability} has no defined response shape")
119
+
120
+
121
+ def _reported(capability: str, config: Config) -> Any:
122
+ key = config_key(capability)
123
+ if key not in config.capabilities:
124
+ raise ConfigError(
125
+ f"the configuration does not report {capability}; add {key!r} under "
126
+ f"[capabilities] in {config.source.path}"
127
+ )
128
+ return config.capabilities[key]
129
+
130
+
131
+ def _operator_lines(operators: Sequence[Mapping[str, Any]]) -> str:
132
+ """One line per operator: its name, then the element types it is supported for.
133
+
134
+ An operator configured with no element types is emitted bare. Section 5.4.1
135
+ states that where the list is empty, the solver is assumed to support the
136
+ operator for all element types it reports through --onnx-element-types, so
137
+ the bare line is the standard's own way of saying "all of them", not "none".
138
+
139
+ That assumption is the reader's to make, which is why the line is left bare
140
+ rather than expanded: expanding it would answer the same question with
141
+ different bytes and settle on a consumer's behalf something the standard
142
+ leaves to their parser.
143
+ """
144
+ return _lines(
145
+ " ".join([operator["name"], *operator.get("element-types", [])])
146
+ for operator in operators
147
+ )
148
+
149
+
150
+ def _lines(values: Iterable[str]) -> str:
151
+ return "".join(f"{value}\n" for value in values)