ESBMC
Efficient SMT-based Context-Bounded Model Checker
Loading...
Searching...
No Matches
numpy_reducer_shared.h
Go to the documentation of this file.
1#pragma once
2
3// Small numeric/AST helpers shared between numpy_call_expr.cpp (the
4// np.<reducer>(...) free-function forms) and function_call/expr.cpp (the
5// a.<reducer>(...) method forms). Both translation units need the exact same
6// literal-axis parsing, "flatten" (axis=None) detection, axis normalization
7// and 1-D result construction -- keeping one copy here avoids the two
8// dispatch paths drifting apart on what counts as a supported spelling.
9
11#include <nlohmann/json.hpp>
14#include <util/irep/expr.h>
15#include <util/irep/std_expr.h>
16#include <algorithm>
17#include <cstddef>
18#include <cstdint>
19#include <set>
20#include <stdexcept>
21#include <string>
22#include <utility>
23#include <vector>
24
26{
27 bool is_int = true;
28 int64_t int_value = 0;
29 double double_value = 0.0;
30};
31
33{
34 return {true, value, static_cast<double>(value)};
35}
36
38{
39 return {false, 0, value};
40}
41
42inline double to_double(const numeric_value &value)
43{
44 return value.is_int ? static_cast<double>(value.int_value)
45 : value.double_value;
46}
47
48inline numeric_value extract_value(const nlohmann::json &arg)
49{
50 if (!arg.contains("_type"))
51 throw std::runtime_error("Invalid JSON: missing _type");
52
53 if (arg["_type"] == "UnaryOp")
54 {
55 if (!arg.contains("operand") || !arg["operand"].contains("value"))
56 throw std::runtime_error("Invalid UnaryOp: missing operand/value");
57
58 auto operand = arg["operand"]["value"];
59 if (operand.is_number_integer())
60 return make_int_value(-operand.get<int64_t>());
61 if (operand.is_number_float())
62 return make_float_value(-operand.get<double>());
63 }
64
65 if (!arg.contains("value"))
66 throw std::runtime_error("Invalid JSON: missing value");
67
68 auto value = arg["value"];
69 if (value.is_boolean())
70 return make_int_value(value.get<bool>() ? 1 : 0);
71 if (value.is_number_integer())
72 return make_int_value(value.get<int64_t>());
73 if (value.is_number_float())
74 return make_float_value(value.get<double>());
75
76 // A non-finite literal arrives with a nulled value and a spelling tag, so
77 // the number checks above all miss it (#7545).
78 if (arg.contains("value_nonfinite"))
79 return make_float_value(
80 nonfinite_float_from_spelling(arg["value_nonfinite"].get<std::string>())
81 ->to_double());
82
83 throw std::runtime_error("Unknown numeric type in JSON");
84}
85
86// The boolean try_extract_* helpers must not depend on catching an exception
87// for control flow: extract_value() raises std::runtime_error on non-numeric
88// input, and relying on that as a flow-control signal is fragile. Pre-check
89// that the payload is numeric and only call extract_value() when it is
90// guaranteed to succeed, so a non-numeric literal (e.g. a str element in
91// numpy.linalg.det's matrix) makes this helper return false cleanly instead
92// of letting the internal "Unknown numeric type" error escape to the user
93// (issue #5206).
94inline bool
95try_extract_numeric_constant(const nlohmann::json &node, numeric_value &out)
96{
97 if (!node.is_object() || !node.contains("_type"))
98 return false;
99
100 const std::string type = node["_type"];
101
102 if (type == "UnaryOp")
103 {
104 if (
105 !node.contains("operand") || !node["operand"].is_object() ||
106 !node["operand"].contains("value"))
107 return false;
108 // extract_value() only negates integer/float operands.
109 const auto &operand = node["operand"]["value"];
110 if (!operand.is_number_integer() && !operand.is_number_float())
111 return false;
112 }
113 else if (type == "Constant")
114 {
115 if (!node.contains("value"))
116 return false;
117 const auto &value = node["value"];
118 if (
119 !value.is_boolean() && !value.is_number_integer() &&
120 !value.is_number_float())
121 return false;
122 }
123 else
124 return false;
125
126 out = extract_value(node);
127 return true;
128}
129
130// True for the JSON shape a Python `None` literal takes in this AST (a
131// Constant node whose value is JSON null) -- NOT a bare JSON null, which
132// never occurs at this position in a well-formed AST.
133inline bool is_json_none_literal(const nlohmann::json &node)
134{
135 return node.is_object() && node.contains("_type") &&
136 node["_type"] == "Constant" && node.contains("value") &&
137 node["value"].is_null();
138}
139
140// True when `call` carries any keyword besides "axis" -- used once an axis=
141// value has already been accepted (or explicitly declined as None), to
142// still reject keepdims/where/out/initial/dtype alongside it.
143inline bool
145{
146 if (!call.contains("keywords"))
147 return false;
148 for (const auto &kw : call["keywords"])
149 if (kw.value("arg", "") != "axis")
150 return true;
151 return false;
152}
153
154// Generalizes numpy_reducer_has_unsupported_keywords_besides_axis to any set
155// of already-accepted keyword names -- e.g. sort/argsort's kind= (and
156// ndarray.argsort()'s stable=) keyword, once those are validated separately
157// by the caller.
159 const nlohmann::json &call,
160 const std::set<std::string> &allowed)
161{
162 if (!call.contains("keywords"))
163 return false;
164 for (const auto &kw : call["keywords"])
165 if (allowed.count(kw.value("arg", "")) == 0)
166 return true;
167 return false;
168}
169
170// numpy.sort()/argsort()'s kind= value: None/"stable"/"mergesort" are
171// accepted no-ops, since the shared conversion-time bubble sort
172// (bubble_sort_numpy_paired) is already stable -- any other kind is
173// rejected explicitly rather than silently applying a different, unstable
174// ordering (ADR-NP principle 3).
176 const nlohmann::json &value,
177 const std::string &function_name)
178{
180 return;
181 if (
182 value.is_object() && value.value("_type", std::string()) == "Constant" &&
183 value.contains("value") && value["value"].is_string())
184 {
185 const std::string kind = value["value"].get<std::string>();
186 if (kind == "stable" || kind == "mergesort")
187 return;
188 }
189 throw std::runtime_error(
190 "TypeError: numpy." + function_name +
191 "() currently supports kind='stable'/'mergesort'/None only");
192}
193
194// ndarray.argsort()'s stable= keyword (numpy 2.0): a literal bool, accepted
195// as a no-op for the same reason kind='stable' is -- the shared sort is
196// already stable regardless of this flag.
198 const nlohmann::json &value,
199 const std::string &function_name)
200{
201 if (
202 !value.is_object() || value.value("_type", std::string()) != "Constant" ||
203 !value.contains("value") || !value["value"].is_boolean())
204 throw std::runtime_error(
205 "TypeError: numpy." + function_name + "() stable must be a literal bool");
206}
207
208// Builds a 1-D array_typet value from already-converted elements.
210 const std::vector<exprt> &elems,
211 const type_handler &th)
212{
213 typet result_type = th.build_array(elems.front().type(), elems.size());
214 exprt value = gen_zero(result_type);
215 for (std::size_t i = 0; i < elems.size(); ++i)
216 value.operands().at(i) = elems[i];
217 return value;
218}
219
220// Normalizes a literal axis against a known rank and validates it lands in
221// range, throwing AxisError otherwise.
222inline long long normalize_reducer_axis(long long axis, std::size_t rank)
223{
224 const long long normalized =
225 axis < 0 ? axis + static_cast<long long>(rank) : axis;
226 if (normalized < 0 || normalized >= static_cast<long long>(rank))
227 throw std::runtime_error(
228 "AxisError: axis " + std::to_string(axis) +
229 " is out of bounds for array of dimension " + std::to_string(rank));
230 return normalized;
231}
232
233// Cap on descriptor-materialized elements a conversion-time sort/argsort
234// bubble-sort network will unroll -- shared between numpy.sort()/argsort()
235// and ndarray.sort()/argsort() so both dispatch paths agree on the same
236// blow-up bound (ADR-NP-003 principle 3).
237constexpr std::size_t max_numpy_sort_elements = 64;
238
239// A conversion-time-unrolled bubble sort over already-converted elements,
240// swapping via if_exprt on `keys` rather than extracting a C++ comparison
241// key -- the same style reduce_numpy_descriptor_values's own min/max
242// branches use, so this works uniformly across every element type get_expr
243// can produce (int/float/bool), concrete or symbolic alike. When `payload`
244// is non-null, every swap decided by `keys` is mirrored onto it (e.g.
245// tracking each element's original index for argsort).
246inline void
247bubble_sort_numpy_paired(std::vector<exprt> &keys, std::vector<exprt> *payload)
248{
249 for (std::size_t pass = 0; pass + 1 < keys.size(); ++pass)
250 {
251 for (std::size_t j = 0; j + pass + 1 < keys.size(); ++j)
252 {
253 binary_relation_exprt out_of_order(keys[j], ">", keys[j + 1]);
254 exprt lo = if_exprt(out_of_order, keys[j + 1], keys[j]);
255 exprt hi = if_exprt(out_of_order, keys[j], keys[j + 1]);
256 keys[j] = lo;
257 keys[j + 1] = hi;
258
259 if (payload != nullptr)
260 {
261 exprt lo_p = if_exprt(out_of_order, (*payload)[j + 1], (*payload)[j]);
262 exprt hi_p = if_exprt(out_of_order, (*payload)[j], (*payload)[j + 1]);
263 (*payload)[j] = lo_p;
264 (*payload)[j + 1] = hi_p;
265 }
266 }
267 }
268}
269
270// Assembles a rank 1 or 2 array_typet value from already-converted,
271// row-major flat elements -- the sort/argsort counterpart of
272// build_1d_numpy_array_value above, extended to rank 2 so an axis-aware
273// result can be reassembled into its original shape. Takes elem_type
274// explicitly (rather than reading elems.front().type()) so a zero-element
275// shape (e.g. transposing a (2, 0)-shaped parameter) does not read past an
276// empty vector; descriptor materialization permits empty arrays, so callers
277// over a parameter/descriptor source cannot assume elems is non-empty the
278// way a rejects-empty caller like sort/argsort's own callers can.
280 const std::vector<std::size_t> &shape,
281 const std::vector<exprt> &elems,
282 const typet &elem_type,
283 const type_handler &th)
284{
285 if (shape.size() == 1)
286 {
287 typet result_type = th.build_array(elem_type, elems.size());
288 exprt value = gen_zero(result_type);
289 for (std::size_t i = 0; i < elems.size(); ++i)
290 value.operands().at(i) = elems[i];
291 return value;
292 }
293
294 const std::size_t cols = shape[1];
295 typet row_type = th.build_array(elem_type, cols);
296 typet result_type = th.build_array(row_type, shape[0]);
297 exprt value = gen_zero(result_type);
298 for (std::size_t row = 0; row < shape[0]; ++row)
299 for (std::size_t col = 0; col < cols; ++col)
300 value.operands().at(row).operands().at(col) = elems[(row * cols) + col];
301 return value;
302}
303
304// Sorts (or argsorts) already-converted, row-major flat elements from
305// build_numpy_descriptor_materialized_elements -- shared by
306// numpy.sort()/numpy.argsort() (numpy_call_expr.cpp) and
307// ndarray.sort()/argsort() (function_call/expr.cpp), the two dispatch paths
308// that both need the exact same axis-aware sort over that descriptor
309// materialization. `flatten` wins over `axis` (axis=None); otherwise axis is
310// normalized against shape's rank and, for a 2-D shape, each row (axis=1) or
311// column (axis=0) is sorted independently, restarting the local index at 0
312// per slice -- matching argmin_argmax_axis_best_index's own per-slice
313// convention. want_indices selects argsort's index-permutation result over
314// sort's value-permutation one.
316 python_converter &converter,
317 const type_handler &th,
318 const std::vector<std::size_t> &shape,
319 std::vector<exprt> elems,
320 bool flatten,
321 long long axis,
322 bool want_indices,
323 const typet &elem_type)
324{
325 auto make_index = [&](std::size_t i) {
326 nlohmann::json node{
327 {"_type", "Constant"},
328 {"value", static_cast<int64_t>(i)},
329 {"kind", nullptr}};
330 return converter.get_expr(node);
331 };
332
333 auto sort_slice = [&](std::vector<exprt> values) -> std::vector<exprt> {
334 if (!want_indices)
335 {
336 bubble_sort_numpy_paired(values, nullptr);
337 return values;
338 }
339 std::vector<exprt> indices;
340 indices.reserve(values.size());
341 for (std::size_t i = 0; i < values.size(); ++i)
342 indices.push_back(make_index(i));
343 bubble_sort_numpy_paired(values, &indices);
344 return indices;
345 };
346
347 if (flatten)
348 return build_1d_numpy_array_value(sort_slice(std::move(elems)), th);
349
350 const long long normalized = normalize_reducer_axis(axis, shape.size());
351 if (shape.size() == 1)
352 return build_1d_numpy_array_value(sort_slice(std::move(elems)), th);
353
354 const std::size_t rows = shape[0];
355 const std::size_t cols = shape[1];
356 std::vector<exprt> out(elems.size());
357
358 if (normalized == 1)
359 {
360 for (std::size_t r = 0; r < rows; ++r)
361 {
362 std::vector<exprt> row(
363 elems.begin() + static_cast<std::ptrdiff_t>(r * cols),
364 elems.begin() + static_cast<std::ptrdiff_t>((r + 1) * cols));
365 std::vector<exprt> sorted_row = sort_slice(std::move(row));
366 std::copy(
367 sorted_row.begin(),
368 sorted_row.end(),
369 out.begin() + static_cast<std::ptrdiff_t>(r * cols));
370 }
371 }
372 else
373 {
374 for (std::size_t c = 0; c < cols; ++c)
375 {
376 std::vector<exprt> col;
377 col.reserve(rows);
378 for (std::size_t r = 0; r < rows; ++r)
379 col.push_back(elems[(r * cols) + c]);
380 std::vector<exprt> sorted_col = sort_slice(std::move(col));
381 for (std::size_t r = 0; r < rows; ++r)
382 out[(r * cols) + c] = sorted_col[r];
383 }
384 }
385
386 return build_numpy_shape_array_value(shape, out, elem_type, th);
387}
Definition std_expr.h:157
Definition expr.h:34
Definition std_expr.h:833
const irep_idt & size() const
Definition irep.h:302
Main converter for transforming Python AST into ESBMC's intermediate representation.
Definition python_converter.h:76
exprt get_expr(const nlohmann::json &element)
Definition converter_expr.cpp:2402
Definition type_handler.h:113
typet build_array(const typet &sub_type, const size_t size) const
This method creates a typet representing a statically sized array.
Definition type_handler.cpp:439
Definition type.h:9
Definition toml.hpp:1531
std::optional< ieee_floatt > nonfinite_float_from_spelling(const std::string &spelling)
The non-finite double spelling names, or empty when it names none.
Definition convert_float_literal.cpp:94
expr2tc gen_zero(const type2tc &type, bool array_as_array_of)
Build the canonical zero value of type.
Definition irep2_utils.cpp:64
constexpr std::size_t max_numpy_sort_elements
Definition numpy_reducer_shared.h:237
bool numpy_reducer_has_unsupported_keywords_besides(const nlohmann::json &call, const std::set< std::string > &allowed)
Definition numpy_reducer_shared.h:158
void bubble_sort_numpy_paired(std::vector< exprt > &keys, std::vector< exprt > *payload)
Definition numpy_reducer_shared.h:247
bool try_extract_numeric_constant(const nlohmann::json &node, numeric_value &out)
Definition numpy_reducer_shared.h:95
exprt build_numpy_shape_array_value(const std::vector< std::size_t > &shape, const std::vector< exprt > &elems, const typet &elem_type, const type_handler &th)
Definition numpy_reducer_shared.h:279
numeric_value make_float_value(double value)
Definition numpy_reducer_shared.h:37
bool numpy_reducer_has_unsupported_keywords_besides_axis(const nlohmann::json &call)
Definition numpy_reducer_shared.h:144
bool is_json_none_literal(const nlohmann::json &node)
Definition numpy_reducer_shared.h:133
double to_double(const numeric_value &value)
Definition numpy_reducer_shared.h:42
long long normalize_reducer_axis(long long axis, std::size_t rank)
Definition numpy_reducer_shared.h:222
void validate_numpy_sort_kind_keyword_value(const nlohmann::json &value, const std::string &function_name)
Definition numpy_reducer_shared.h:175
void validate_numpy_stable_bool_keyword_value(const nlohmann::json &value, const std::string &function_name)
Definition numpy_reducer_shared.h:197
numeric_value make_int_value(int64_t value)
Definition numpy_reducer_shared.h:32
exprt build_1d_numpy_array_value(const std::vector< exprt > &elems, const type_handler &th)
Definition numpy_reducer_shared.h:209
exprt build_numpy_sort_or_argsort_result(python_converter &converter, const type_handler &th, const std::vector< std::size_t > &shape, std::vector< exprt > elems, bool flatten, long long axis, bool want_indices, const typet &elem_type)
Definition numpy_reducer_shared.h:315
numeric_value extract_value(const nlohmann::json &arg)
Definition numpy_reducer_shared.h:48
Definition numpy_reducer_shared.h:26
bool is_int
Definition numpy_reducer_shared.h:27
double double_value
Definition numpy_reducer_shared.h:29
int64_t int_value
Definition numpy_reducer_shared.h:28