ESBMC
Efficient SMT-based Context-Bounded Model Checker
Loading...
Searching...
No Matches
annotation_conversion.inl
Go to the documentation of this file.
1// Out-of-class member definitions for `python_annotation<Json>` that
2// implement type inference. This is the bulk of the annotation pass —
3// every reach into the JSON AST that asks "what type is this?" lives
4// here. Definitions are kept in topological order: leaf inspectors
5// first, then the dispatch entrypoints that call into them.
6//
7// This file is `#include`d from python_annotation.h after the class
8// definition. Bodies are verbatim from the previous in-class
9// definitions; only the `template <class Json>` line and the
10// `python_annotation<Json>::` qualifier are new. No semantic change.
11
12#pragma once
13
14// Implemented in converter_stmt.cpp; declared there (via python_converter.h)
15// for python_converter/numpy_call_expr, and forward-declared here so this
16// template-only translation unit can share the same numpy-alias resolution
17// without pulling in python_converter.h (which this file predates in the
18// pipeline and must not depend on).
20 const nlohmann::json &ast,
21 const std::string &name);
22
23// ---------- leaf inspectors ----------
24
25template <class Json>
26std::string python_annotation<Json>::get_current_func_name()
27{
28 return current_func_name_context_;
29}
30
31template <class Json>
32std::string python_annotation<Json>::get_type_from_constant(const Json &element)
33{
34 if (element.contains("esbmc_type_annotation"))
35 return element["esbmc_type_annotation"].template get<std::string>();
36
37 auto rhs = element["value"];
38
39 if (rhs.is_number_integer() || rhs.is_number_unsigned())
40 return "int";
41 if (rhs.is_number_float())
42 return "float";
43 if (rhs.is_boolean())
44 return "bool";
45 if (rhs.is_string())
46 return "str";
47 if (rhs.is_null())
48 return "NoneType";
49
50 return std::string();
51}
52
53template <class Json>
54std::string python_annotation<Json>::get_type_from_lhs(
55 const std::string &id,
56 const Json &body)
57{
58 // Search for LHS annotation in the current scope (e.g., while/if block)
59 Json node = find_annotated_assign(id, body["body"]);
60
61 // Fall back to the current function
62 if (node.empty() && current_func != nullptr)
63 node = find_annotated_assign(id, (*current_func)["body"]);
64
65 // Closure capture: fall back to the enclosing function before the
66 // global scope. See find_var_node_for_inference for an analogous pattern.
67 if (node.empty() && parent_func != nullptr)
68 node = find_annotated_assign(id, (*parent_func)["body"]);
69
70 // Fall back to variables in the global scope
71 if (node.empty())
72 node = find_annotated_assign(id, ast_["body"]);
73
74 // Check if node is empty first
75 if (node.empty())
76 return "";
77
78 // Check if "annotation" field exists and is not null
79 if (!node.contains("annotation") || node["annotation"].is_null())
80 return "";
81
82 // Check if "annotation"."id" field exists and is not null
83 if (!node["annotation"].contains("id") || node["annotation"]["id"].is_null())
84 return "";
85
86 // Safe to access the nested field now
87 return node["annotation"]["id"];
88}
89
90template <class Json>
91std::string python_annotation<Json>::get_list_subtype(const Json &list)
92{
93 std::string list_subtype;
94
95 if (
96 list["_type"] == "Call" && list.contains("func") &&
97 list["func"].contains("attr") && list["func"]["attr"] == "array")
98 return get_list_subtype(list["args"][0]);
99
100 if (!list.contains("elts"))
101 return "";
102
103 // A complex literal's value is its spelling, a string.
104 auto element_type = [this](const Json &elem) -> std::string {
105 if (elem.value("esbmc_type_annotation", std::string()) == "complex")
106 return "complex";
107 return elem.contains("value") ? get_type_from_json(elem["value"]) : "";
108 };
109
110 for (const auto &elem : list["elts"])
111 {
112 const std::string type = element_type(elem);
113 if (list_subtype.empty())
114 list_subtype = type;
115 else if (!type.empty() && type != list_subtype)
116 throw std::runtime_error("Multiple typed lists are not supported\n");
117 }
118
119 return list_subtype;
120}
121
122template <class Json>
123bool python_annotation<Json>::extract_type_info(
124 const Json &annotation,
125 std::string &base_type,
126 std::string &element_type)
127{
128 // Handle simple Name annotations
129 if (annotation.contains("id"))
130 {
131 std::string full_type = annotation["id"];
132
133 // Parse generic notation such as "list[dict]"
134 size_t bracket_pos = full_type.find('[');
135 if (bracket_pos != std::string::npos)
136 {
137 base_type = full_type.substr(0, bracket_pos);
138 element_type =
139 full_type.substr(bracket_pos + 1, full_type.length() - bracket_pos - 2);
140 }
141 else
142 base_type = full_type;
143
144 return true;
145 }
146
147 // Handle generic annotations represented as Subscript nodes, e.g. the
148 // `list[int]` written `Subscript(value=Name("list"), slice=Name("int"))`.
149 // The previous Name-only path missed these, so an annotated `list[T]`
150 // without a usable initializer degraded to "Any" (Fixes #5122 review).
151 if (
152 annotation.contains("_type") && annotation["_type"] == "Subscript" &&
153 annotation.contains("value") && annotation["value"].contains("id"))
154 {
155 base_type = annotation["value"]["id"];
156
157 // Only a Name slice yields a concrete element type (list[int]). A
158 // Subscript slice (list[list[int]]) or Tuple slice (dict[K, V]) is left
159 // unresolved, matching the prior behaviour for those shapes.
160 if (annotation.contains("slice") && annotation["slice"].contains("id"))
161 element_type = annotation["slice"]["id"];
162
163 return true;
164 }
165
166 return false;
167}
168
169template <class Json>
170std::string python_annotation<Json>::infer_dict_value_type(const Json &var_node)
171{
172 if (!var_node.contains("value") || var_node["value"].is_null())
173 return "Any";
174
175 const Json &dict_init = var_node["value"];
176
177 // Handle function calls that return dict[K, V]
178 if (dict_init["_type"] == "Call" && dict_init["func"]["_type"] == "Name")
179 {
180 Json func_def = find_function_recursive(
181 dict_init["func"]["id"].template get<std::string>(), ast_["body"]);
182 if (
183 !func_def.empty() && func_def.contains("returns") &&
184 func_def["returns"]["_type"] == "Subscript" &&
185 func_def["returns"]["value"]["id"] == "dict" &&
186 func_def["returns"]["slice"]["elts"].size() >= 2)
187 {
188 const Json &val_type = func_def["returns"]["slice"]["elts"][1];
189 if (val_type["_type"] == "Subscript" && val_type["value"].contains("id"))
190 return val_type["value"]["id"].template get<std::string>();
191 }
192 }
193
194 // Handle dict initialized from function call
195 if (
196 dict_init["_type"] == "Dict" && dict_init.contains("values") &&
197 !dict_init["values"].empty())
198 {
199 const Json &first_value = dict_init["values"][0];
200 std::string value_type = get_argument_type(first_value);
201
202 if (!value_type.empty())
203 return value_type;
204 }
205
206 return "Any";
207}
208
209template <class Json>
210std::string
211python_annotation<Json>::subscript_type_from_call(const Json &subscript_node)
212{
213 const Json &call = subscript_node["value"];
214 if (
215 call["_type"] != "Call" || call["func"]["_type"] != "Name" ||
216 config.options.get_bool_option("override-return-annotation"))
217 return "Any";
218
219 const Json func_def = find_function_recursive(
220 call["func"]["id"].template get<std::string>(), ast_["body"]);
221 if (func_def.empty() || !func_def.contains("returns"))
222 return "Any";
223
224 const Json &ret = func_def["returns"];
225 if (
226 !ret.is_object() || ret.value("_type", "") != "Subscript" ||
227 ret["value"].value("id", "") != "list" || !ret.contains("slice"))
228 return "Any";
229
230 const std::string elem = json_utils::render_annotation_type(ret["slice"]);
231 if (elem.empty())
232 return "Any";
233 return subscript_node["slice"].value("_type", "") == "Slice"
234 ? "list[" + elem + "]"
235 : elem;
236}
237
238template <class Json>
239std::string python_annotation<Json>::subscript_type_from_list_literal(
240 const Json &subscript_node)
241{
242 const Json &elts = subscript_node["value"]["elts"];
243 if (elts.empty() || subscript_node["slice"].value("_type", "") == "Slice")
244 return "Any";
245
246 const std::string elem = get_argument_type(elts[0]);
247 for (const Json &e : elts)
248 if (get_argument_type(e) != elem)
249 return "Any";
250 return elem.empty() ? "Any" : elem;
251}
252
255inline std::string
256unpacked_name_element_type(const std::string &type, size_t index)
257{
258 if (type.empty() || type.back() != ']')
259 return "Any";
260 if (type.rfind("list[", 0) == 0)
261 return type.substr(5, type.size() - 6);
262 if (type.rfind("tuple[", 0) != 0)
263 return "Any";
264 const auto elems =
265 json_utils::split_top_level_params(type.substr(6, type.size() - 7));
266 return index < elems.size() ? elems[index] : "Any";
267}
268
269template <class Json>
270std::string python_annotation<Json>::resolve_subscript_type(
271 const Json &subscript_node,
272 const Json &body,
273 bool nested)
274{
275 const Json &base = subscript_node["value"];
276
277 // Recursively resolve nested subscripts
278 if (base["_type"] == "Subscript")
279 return resolve_subscript_type(base, body, true);
280
281 if (base["_type"] == "List")
282 return subscript_type_from_list_literal(subscript_node);
283
284 if (base["_type"] != "Name")
285 return subscript_type_from_call(subscript_node);
286
287 std::string var_name = base["id"];
288 Json var_node =
289 json_utils::find_var_decl(var_name, get_current_func_name(), ast_);
290
291 if (
292 var_node.empty() || !var_node.contains("annotation") ||
293 var_node["annotation"].is_null())
294 return "Any";
295
296 std::string base_type, element_type;
297
298 if (!extract_type_info(var_node["annotation"], base_type, element_type))
299 return "Any";
300
301 // Dict subscript: infer value type from initialization
302 if (base_type == "dict")
303 return infer_dict_value_type(var_node);
304
305 // List subscript
306 if (base_type == "list")
307 {
308 // A Name slice referencing a *list*-typed variable is fancy indexing
309 // through a variable (`idx = [0, 2]; a[idx]`); unlike a Name slice
310 // referencing a scalar variable (plain `a[i]`), it selects multiple
311 // elements and stays list-typed, same as a literal index list.
312 bool slice_is_list_var = false;
313 if (
314 subscript_node.contains("slice") &&
315 subscript_node["slice"].contains("_type") &&
316 subscript_node["slice"]["_type"] == "Name" &&
317 subscript_node["slice"].contains("id"))
318 {
319 const std::string idx_name = subscript_node["slice"]["id"];
320 Json idx_node =
321 json_utils::find_var_decl(idx_name, get_current_func_name(), ast_);
322 std::string idx_base_type, idx_element_type;
323 if (
324 !idx_node.empty() && idx_node.contains("annotation") &&
325 !idx_node["annotation"].is_null() &&
326 extract_type_info(
327 idx_node["annotation"], idx_base_type, idx_element_type) &&
328 idx_base_type == "list")
329 slice_is_list_var = true;
330 // No (or non-list) annotation resolved -- idx may still be an
331 // unannotated `idx = [0, 2]` whose type is only inferable from its
332 // initializer; a literal list value is as unambiguous a signal as an
333 // explicit annotation.
334 else if (
335 !idx_node.empty() && idx_node.contains("value") &&
336 idx_node["value"].value("_type", std::string()) == "List")
337 slice_is_list_var = true;
338 }
339
340 // A `Slice` (`a[1:3]`) or a literal index list (`a[[0, 2]]`, NumPy fancy
341 // indexing) both select multiple elements and stay list-typed; only a
342 // single Name/Constant/UnaryOp index narrows to the element type.
343 const bool is_multi_result =
344 (subscript_node.contains("slice") &&
345 subscript_node["slice"].contains("_type") &&
346 (subscript_node["slice"]["_type"] == "Slice" ||
347 subscript_node["slice"]["_type"] == "List")) ||
348 slice_is_list_var;
349
350 // First try to use the element_type from annotation (e.g., list[int])
351 if (!element_type.empty())
352 return is_multi_result ? "list[" + element_type + "]" : element_type;
353
354 // Try to infer from initialization if available
355 if (var_node.contains("value") && !var_node["value"].is_null())
356 {
357 std::string inferred = get_list_subtype(var_node["value"]);
358 if (!inferred.empty())
359 return is_multi_result ? "list[" + inferred + "]" : inferred;
360 }
361
362 // Last resort: return Any for unknown list element types
363 return is_multi_result ? "list" : "Any";
364 }
365
366 // String subscript: str[index] returns str
367 if (base_type == "str")
368 return "str";
369
370 // tuple[A, B, ...] at a constant index yields that member's type (#8928).
371 // This pass returns the inner subscript's type for a whole chain, so a
372 // nested read such as t[0][1] is left untyped.
373 if (base_type == "tuple" && nested)
374 return "Any";
375 const Json &slice = subscript_node.value("slice", Json());
376 if (
377 base_type == "tuple" && slice.value("_type", "") == "Constant" &&
378 slice["value"].is_number_unsigned())
379 {
380 const std::string elem = unpacked_name_element_type(
381 json_utils::render_annotation_type(var_node["annotation"]),
382 slice["value"].template get<size_t>());
383 if (elem != "Any")
384 return elem;
385 }
386
387 // For other types, return the base type
388 return base_type;
389}
390
391template <class Json>
392bool python_annotation<Json>::is_imported_name_in_body(
393 const std::string &name,
394 const Json &stmts) const
395{
396 // Imported names inside a function body are visible to later calls in that
397 // same body, even though they are not present in the module AST.
398 for (const auto &stmt : stmts)
399 {
400 if (!stmt.contains("_type"))
401 continue;
402
403 const std::string &stype = stmt["_type"];
404 if ((stype == "ImportFrom" || stype == "Import") && stmt.contains("names"))
405 {
406 for (const auto &alias : stmt["names"])
407 {
408 if (
409 alias.contains("name") &&
410 alias["name"].template get<std::string>() == name)
411 return true;
412 if (
413 alias.contains("asname") && !alias["asname"].is_null() &&
414 alias["asname"].template get<std::string>() == name)
415 return true;
416 }
417 }
418 }
419
420 return false;
421}
422
423template <class Json>
424bool python_annotation<Json>::is_imported_name(const std::string &name) const
425{
426 return is_imported_name_in_body(name, ast_["body"]) ||
427 (current_func != nullptr && (*current_func).contains("body") &&
428 is_imported_name_in_body(name, (*current_func)["body"]));
429}
430
431template <class Json>
432std::string python_annotation<Json>::get_object_name(
433 const Json &call,
434 const std::string &prefix)
435{
436 if (call["value"]["_type"] == "Attribute")
437 {
438 std::string append = call["value"]["attr"].template get<std::string>();
439 if (!prefix.empty())
440 append = prefix + std::string(".") + append;
441
442 return get_object_name(call["value"], append);
443 }
444 std::string obj_name("");
445 if (!prefix.empty())
446 {
447 obj_name = prefix + std::string(".");
448 }
449
450 // Handle method calls on constant literals
451 // Python allows: " ".join(l), 123.to_bytes(), etc.
452 // Before this fix, accessing call["value"]["id"] would crash because
453 // Constant nodes don't have an "id" field
454 if (call["value"]["_type"] == "Constant")
455 return get_type_from_constant(call["value"]);
456
457 // Handle method calls on temporary instances: A().method()
458 // A() is a Call node with no "id" field; return the class name so that
459 // get_type_from_method() can look up the method in the class definition.
460 if (
461 call["value"]["_type"] == "Call" && call["value"].contains("func") &&
462 call["value"]["func"]["_type"] == "Name" &&
463 call["value"]["func"].contains("id"))
464 return call["value"]["func"]["id"].template get<std::string>();
465
466 // Handle chained method calls: B().g().f() where B().g() is itself a method
467 // call returning an object. Recursively resolve the return type of B().g()
468 // so that the outer call .f() can look up the method in the right class.
469 if (
470 call["value"]["_type"] == "Call" && call["value"].contains("func") &&
471 call["value"]["func"]["_type"] == "Attribute")
472 return get_type_from_method(call["value"]);
473
474 // Handle normal Name values (variable references)
475 if (!call["value"].contains("id"))
476 return "";
477
478 obj_name += call["value"]["id"].template get<std::string>();
479 if (obj_name.find('.') != std::string::npos)
480 obj_name = invert_substrings(obj_name);
481
482 return json_utils::get_object_alias(ast_, obj_name);
483}
484
485template <class Json>
486std::string python_annotation<Json>::get_string_method_return_type(
487 const std::string &method) const
488{
489 if (
490 method == "join" || method == "lower" || method == "upper" ||
491 method == "strip" || method == "lstrip" || method == "rstrip" ||
492 method == "format" || method == "replace")
493 return "str";
494
495 if (
496 method == "startswith" || method == "endswith" || method == "isdigit" ||
497 method == "isalpha" || method == "isspace" || method == "islower" ||
498 method == "isupper")
499 return "bool";
500
501 if (method == "find" || method == "rfind")
502 return "int";
503
504 if (method == "split" || method == "rsplit")
505 return "list";
506
507 // str.encode() returns a bytes object, not str.
508 if (method == "encode")
509 return "bytes";
510
511 // partition()/rpartition() return a 3-tuple (before, sep, after). Map to
512 // "tuple", which resolves to an empty type so the concrete struct type of the
513 // 3-tuple produced by handle_string_partition is copied onto the target
514 // symbol. Mapping it to the default "str" mistypes the target as a scalar
515 // char, which makes len()/subscript on the result wrong (unsound — proves
516 // false assertions, #5114).
517 if (method == "partition" || method == "rpartition")
518 return "tuple";
519
520 // Keep previous behavior for unmapped string methods.
521 return "str";
522}
523
524template <class Json>
525std::string python_annotation<Json>::match_literal_argument(
526 const Json &call_node,
527 std::vector<Json> overloads) const
528{
529 if (overloads.empty())
530 return "";
531
532 // Try to match based on literal arguments
533 if (!call_node.contains("args") || call_node["args"].empty())
534 return "";
535
536 for (const auto &overload : overloads)
537 {
538 if (!overload.contains("args") || !overload["args"].contains("args"))
539 continue;
540
541 const auto &params = overload["args"]["args"];
542 const auto &call_args = call_node["args"];
543
544 // Try to match first parameter (literal type check)
545 if (params.size() > 0 && call_args.size() > 0)
546 {
547 const auto &param_annotation = params[0]["annotation"];
548 const auto &call_arg = call_args[0];
549
550 // Check for Literal["foo"] pattern
551 if (
552 param_annotation["_type"] == "Subscript" &&
553 param_annotation["value"]["id"] == "Literal" &&
554 param_annotation["slice"]["_type"] == "Constant")
555 {
556 std::string literal_value =
557 param_annotation["slice"]["value"].template get<std::string>();
558
559 // Check if call argument matches
560 if (
561 call_arg["_type"] == "Constant" && call_arg["value"].is_string() &&
562 call_arg["value"].template get<std::string>() == literal_value)
563 {
564 // Found matching overload, return its type
565 if (
566 overload.contains("returns") && !overload["returns"].is_null() &&
567 overload["returns"].contains("id"))
568 {
569 return overload["returns"]["id"];
570 }
571 }
572 }
573 }
574 }
575
576 return "";
577}
578
579// Find the best matching overload
580template <class Json>
581std::string python_annotation<Json>::resolve_overload_return_type(
582 const std::string &func_name,
583 const Json &call_node) const
584{
585 std::vector<Json> overloads;
586
587 // Find all overload definitions
588 for (const Json &elem : ast_["body"])
589 {
590 if (
591 elem["_type"] == "FunctionDef" && elem["name"] == func_name &&
593 overloads.push_back(elem);
594 }
595
596 return match_literal_argument(call_node, overloads);
597}
598
599template <class Json>
600std::string
601python_annotation<Json>::resolve_object_class_name(const std::string &obj)
602{
603 auto read_name_id = [](const Json &annotation) -> std::string {
604 if (
605 annotation.is_object() && !annotation.is_null() &&
606 annotation.contains("_type") && annotation["_type"] == "Name" &&
607 annotation.contains("id") && annotation["id"].is_string())
608 return annotation["id"].template get<std::string>();
609 return "";
610 };
611
612 if (
613 current_func != nullptr && (*current_func).contains("args") &&
614 (*current_func)["args"].contains("args"))
615 {
616 Json param = find_annotated_assign(obj, (*current_func)["args"]["args"]);
617 if (param.contains("annotation"))
618 {
619 std::string id = read_name_id(param["annotation"]);
620 if (!id.empty())
621 return id;
622 }
623 }
624
625 Json var = json_utils::find_var_decl(obj, get_current_func_name(), ast_);
626 if (var.empty())
627 return "";
628
629 if (var.contains("annotation"))
630 {
631 std::string id = read_name_id(var["annotation"]);
632 if (!id.empty())
633 return id;
634 }
635
636 if (
637 var.contains("value") && var["value"].is_object() &&
638 var["value"].contains("_type") && var["value"]["_type"] == "Call" &&
639 var["value"].contains("func") && var["value"]["func"].is_object() &&
640 var["value"]["func"].contains("_type") &&
641 var["value"]["func"]["_type"] == "Name" &&
642 var["value"]["func"].contains("id") &&
643 var["value"]["func"]["id"].is_string())
644 return var["value"]["func"]["id"].template get<std::string>();
645
646 return "";
647}
648
649template <class Json>
650std::string python_annotation<Json>::infer_unpacked_element_type(
651 const Json &rhs,
652 size_t index)
653{
654 if (!rhs.is_object() || !rhs.contains("_type"))
655 return "Any";
656
657 const std::string &kind = rhs["_type"];
658
659 if (
660 (kind == "Tuple" || kind == "List") && rhs.contains("elts") &&
661 index < rhs["elts"].size())
662 {
663 std::string t = get_argument_type(rhs["elts"][index]);
664 return t.empty() ? "Any" : t;
665 }
666
667 // A target typed "Any" makes `return x + 1` a float, which loses precision
668 // past 2**53 (GitHub #8358).
669 if (kind == "Name")
670 {
671 // get_argument_type flattens a parameter's `tuple[int, int]` to "tuple".
672 const Json decl =
673 find_var_node_for_inference(rhs["id"].template get<std::string>());
674 const std::string type =
675 has_annotation(decl)
676 ? json_utils::render_annotation_type(decl["annotation"])
677 : get_argument_type(rhs);
678 return unpacked_name_element_type(type, index);
679 }
680
681 if (
682 kind == "Attribute" && rhs.contains("value") && rhs["value"].is_object() &&
683 rhs["value"]["_type"] == "Name" && rhs.contains("attr"))
684 {
685 std::string cls =
686 resolve_object_class_name(rhs["value"]["id"].template get<std::string>());
687 if (cls.empty())
688 return "Any";
690 cls, rhs["attr"].template get<std::string>(), ast_["body"]);
691 if (attr_rhs.empty())
692 return "Any";
693 return infer_unpacked_element_type(attr_rhs, index);
694 }
695
696 return "Any";
697}
698
700inline std::string
701subscript_result_type(const std::string &container, bool is_slice)
702{
703 // The trailing ']' is redundant against every current producer, and is
704 // kept only to hold the invariant the substr calls rely on.
705 if (container.size() < 6 || container.back() != ']')
706 return "";
707 const std::string params = container.substr(5, container.size() - 6);
708 if (container.rfind("list[", 0) == 0)
709 return is_slice ? container : params;
710 // A dict lookup yields the value type (GitHub #8358).
711 const auto kv = json_utils::split_top_level_params(params);
712 if (container.rfind("dict[", 0) == 0 && !is_slice && kv.size() == 2)
713 return kv[1];
714 return "";
715}
716
719template <class Json>
720bool is_empty_list_literal(const Json &node)
721{
722 if (node.empty() || !node.contains("value") || node["value"].is_null())
723 return false;
724 const Json &value = node["value"];
725 return value.value("_type", "") == "List" &&
726 (!value.contains("elts") || value["elts"].empty());
727}
728
731inline bool has_bare_tuple(const std::string &type)
732{
733 for (size_t pos = type.find("tuple"); pos != std::string::npos;
734 pos = type.find("tuple", pos + 1))
735 {
736 const size_t end = pos + 5;
737 const bool starts = pos == 0 || type[pos - 1] == '[' ||
738 type[pos - 1] == ' ' || type[pos - 1] == ',';
739 if (starts && (end == type.size() || type[end] == ']' || type[end] == ','))
740 return true;
741 }
742 return false;
743}
744
747inline bool is_unresolved_arg_type(const std::string &type)
748{
749 return type.empty() || type == "NoneType" || type == "Any" ||
750 has_bare_tuple(type);
751}
752
757template <class Json>
758std::string python_annotation<Json>::list_copy_type(
759 const std::string &func_name,
760 const Json &call)
761{
762 if (
763 (func_name != "list" && func_name != "tuple") || !call.contains("args") ||
764 call["args"].size() != 1)
765 return "";
766 const Json &iterable = call["args"][0];
767 const std::string source = get_argument_type(iterable);
768 if (source == "range")
769 return "list[int]";
770 if (source.rfind("list[", 0) == 0)
771 return source;
772 return dict_view_list_type(iterable);
773}
774
777template <class Json>
778std::string python_annotation<Json>::dict_view_list_type(const Json &call)
779{
780 if (
781 call.value("_type", "") != "Call" || !call["args"].empty() ||
782 call["func"].value("_type", "") != "Attribute")
783 return "";
784 const std::string dict_type = get_argument_type(call["func"]["value"]);
785 if (dict_type.rfind("dict[", 0) != 0 || dict_type.back() != ']')
786 return "";
788 dict_type.substr(5, dict_type.size() - 6));
789 if (kv.size() != 2)
790 return "";
791 const std::string method = call["func"].value("attr", "");
792 if (method == "keys")
793 return "list[" + kv[0] + "]";
794 if (method == "values")
795 return "list[" + kv[1] + "]";
796 return "";
797}
798
799// Infer generic dict type from literal: {key: value, ...}
800template <class Json>
801std::string
802python_annotation<Json>::get_dict_literal_type(const Json &dict_value)
803{
804 if (
805 !dict_value.contains("keys") || !dict_value.contains("values") ||
806 dict_value["keys"].empty() || dict_value["values"].empty())
807 return "dict";
808
809 // A `**` unpack (null key) contributes another dict's entries, so the
810 // first entry does not stand for the rest.
811 const Json &keys = dict_value["keys"];
812 if (std::any_of(keys.begin(), keys.end(), [](const Json &key) {
813 return key.is_null();
814 }))
815 return "dict";
816
817 // Get types of first key and value
818 std::string key_type = get_argument_type(dict_value["keys"][0]);
819 std::string value_type = get_argument_type(dict_value["values"][0]);
820
821 // Only infer generic dict type for non-nested types
822 // Nested structures (such as list[dict]) are better handled
823 // with plain 'dict' so the converter can use dynamic type info
824 bool key_is_simple = key_type.find('[') == std::string::npos;
825 bool value_is_simple = value_type.find('[') == std::string::npos;
826 if (
827 !key_type.empty() && !value_type.empty() && key_is_simple &&
828 value_is_simple)
829 return "dict[" + key_type + ", " + value_type + "]";
830 return "dict";
831}
832
833// ---------- dispatchers ----------
834
835// The type of the value a call such as abs(a - b), len(list) or A()
836// returns, or "" when unknown.
837template <class Json>
838std::string python_annotation<Json>::get_call_type(const Json &arg)
839{
840 // A module-qualified nondet call, such as _sv_verifier.nondet_float(),
841 // returns the type its name ends in, as the unqualified form does.
842 const std::string attr = arg["func"].value("attr", "");
843 if (arg["func"]["_type"] == "Attribute" && attr.rfind("nondet_", 0) == 0)
844 return attr.substr(attr.find_last_of('_') + 1);
845
846 if (arg["func"]["_type"] == "Name")
847 {
848 const std::string &func_name = arg["func"]["id"];
849
850 // Class constructor call: A() produces an instance of A
851 // Resolve possible import aliases before checking if this is a class
852 const std::string class_name =
853 json_utils::get_object_alias(ast_, func_name);
854 if (json_utils::is_class(class_name, ast_))
855 return class_name;
856 // A built-in exception is a class too, so ValueError("x") and 1 at two
857 // call sites widen the parameter rather than typing it int (#8766).
858 if (type_utils::is_python_exceptions(class_name))
859 return class_name;
860
861 // hash() returns bytes only for the consensus spec's own
862 // hash(data: bytes); on any other argument it is the real builtin,
863 // which returns int (see is_generic_hash_call in function_call/expr.cpp).
864 if (
865 func_name == "hash" && arg.contains("args") && !arg["args"].empty() &&
866 get_argument_type(arg["args"][0]) != "bytes")
867 return "int";
868
869 const std::string copied = list_copy_type(func_name, arg);
870 if (!copied.empty())
871 return copied;
872
873 if (builtin_functions().count(func_name))
874 return get_builtin_call_type(arg);
875
876 // For user-defined functions, try to get return type
877 try
878 {
879 return get_function_return_type(func_name, ast_);
880 }
881 catch (std::runtime_error &)
882 {
883 // Function not found, return empty
884 return "";
885 }
886 }
887
888 return "";
889}
890
894template <class Json>
895std::string
896python_annotation<Json>::subscript_annotation_type(const Json &annotation)
897{
898 const std::string full = json_utils::render_annotation_type(annotation);
899 if (annotation["value"]["id"] == "dict" && !full.empty())
900 return full;
901 return json_utils::get_annotation_type_name(annotation);
902}
903
906template <class Json>
907std::string python_annotation<Json>::container_literal_type(
908 const std::string &base_type,
909 const Json &value)
910{
911 if (base_type == "list" && value["_type"] == "List")
912 return get_list_type_from_literal(value);
913 if (base_type == "dict" && value["_type"] == "Dict")
914 return get_argument_type(value);
915 return "";
916}
917
918template <class Json>
919std::string python_annotation<Json>::get_argument_type(const Json &arg)
920{
921 // Name/Subscript/List arms resolve a name to its bound value and recurse,
922 // and find_var_node_for_inference is flow-insensitive, so a self-referential
923 // binding (`a = b[0]; b = [a]`) has no ordering that breaks the cycle.
924 // Bound the depth rather than the shape: a legitimate annotation nests far
925 // shallower than this.
926 static constexpr unsigned max_depth = 32;
927 if (arg_type_depth_ >= max_depth)
928 return "";
929 struct depth_guardt
930 {
931 unsigned &d;
932 explicit depth_guardt(unsigned &depth) : d(depth)
933 {
934 ++d;
935 }
936 ~depth_guardt()
937 {
938 --d;
939 }
940 } guard(arg_type_depth_);
941
942 // A `**` unpack inside a dict literal (e.g. {**m}) serialises its key as a
943 // null entry — there is no AST node and hence no inferable type. Returning a
944 // safe default keeps the annotation pass from dereferencing a null json.
945 if (!arg.is_object())
946 return "Any";
947
948 if (arg["_type"] == "Constant")
949 return get_type_from_constant(arg);
950 else if (arg["_type"] == "Subscript")
951 {
952 // Handle subscripts like tokens[0] and slices like tokens[:1].
953 const auto &val = arg["value"];
954 if (val["_type"] == "Name")
955 {
956 std::string var_name = val["id"].template get<std::string>();
957
958 // find_var_node_for_inference resolves to the first textual binding, so
959 // on a rebound name every arm below would type the element from a dead
960 // one. Decline instead -- no inference is what shipped before #7359.
961 if (is_rebound_in_scope(var_name))
962 return "";
963
964 Json var_node = find_var_node_for_inference(var_name);
965 const bool is_slice =
966 arg.contains("slice") && arg["slice"].value("_type", "") == "Slice";
967
968 if (has_annotation(var_node))
969 {
970 const auto &annot = var_node["annotation"];
971 // list[T] -> return T
972 if (
973 annot.contains("_type") && annot["_type"] == "Subscript" &&
974 annot.contains("value") && annot["value"].contains("id") &&
975 annot["value"]["id"] == "list")
976 {
977 // Render the element type in full: a nested generic (list[list[int]],
978 // list[tuple[int, int]]) has no "id" to read, and reporting "Any" for
979 // it leaves the parameter unannotated, which the converter then
980 // guesses is a list (GitHub #7359).
981 const std::string elem =
982 annot.contains("slice")
983 ? json_utils::render_annotation_type(annot["slice"])
984 : std::string();
985
986 if (
987 arg.contains("slice") && arg["slice"].contains("_type") &&
988 arg["slice"]["_type"] == "Slice")
989 return elem.empty() ? "list" : "list[" + elem + "]";
990
991 return elem.empty() ? "Any" : elem;
992 }
993 // Simple name annotation (e.g., list without subtype)
994 if (annot.contains("id") && annot["id"] == "list")
995 return bare_list_subscript_type(var_node, var_name, is_slice);
996 }
997
998 // No annotation yet: this pass runs before plain assignments are
999 // rewritten to annotated ones, so let the Name arm below recover the
1000 // type from the bound value, including the element type of an empty
1001 // list built by appends (GitHub #7359).
1002 if (
1003 !var_node.empty() && var_node.contains("value") &&
1004 !var_node["value"].is_null())
1005 {
1006 return subscript_result_type(get_argument_type(val), is_slice);
1007 }
1008 }
1009 return "";
1010 }
1011 else if (arg["_type"] == "Slice")
1012 return "slice";
1013 else if (arg["_type"] == "UnaryOp")
1014 {
1015 // Handle unary operations like -5, +3, not True
1016 if (arg.contains("operand"))
1017 return get_argument_type(arg["operand"]);
1018 }
1019 else if (arg["_type"] == "Name")
1020 {
1021 const std::string &var_name = arg["id"];
1022
1023 // Try to find the type of the variable in current scope
1024 Json var_node = find_var_node_for_inference(var_name);
1025
1026 // Extract type from the found variable node
1027 if (has_annotation(var_node))
1028 {
1029 // Handle arg nodes (function parameters)
1030 if (var_node["_type"] == "arg")
1031 {
1032 return json_utils::get_annotation_type_name(var_node["annotation"]);
1033 }
1034 // Handle generic type annotations like list[int] (Subscript nodes)
1035 else if (
1036 var_node["annotation"].contains("_type") &&
1037 var_node["annotation"]["_type"] == "Subscript" &&
1038 var_node["annotation"].contains("value") &&
1039 var_node["annotation"]["value"].contains("id"))
1040 {
1041 return subscript_annotation_type(var_node["annotation"]);
1042 }
1043 // Handle simple type annotations like int, str (Name nodes)
1044 else if (var_node["annotation"].contains("id"))
1045 {
1046 std::string base_type =
1047 var_node["annotation"]["id"].template get<std::string>();
1048 // If annotation is just "list"/"dict"/"set" without element type, try
1049 // to infer from value
1050 if (
1051 (base_type == "list" || base_type == "dict" || base_type == "set") &&
1052 var_node.contains("value") && !var_node["value"].is_null())
1053 {
1054 const std::string full_type =
1055 container_literal_type(base_type, var_node["value"]);
1056 if (!full_type.empty())
1057 return full_type;
1058 }
1059 return refine_any_from_value(base_type, var_node);
1060 }
1061 }
1062
1063 if (
1064 !var_node.empty() && var_node.contains("value") &&
1065 !var_node["value"].is_null())
1066 {
1067 // A name bound to an *empty* list literal loses its element type. This
1068 // is how list comprehensions lower (`tmp = []; tmp.append(elt)`), so
1069 // recover `list[T]` from the first append before falling back to the
1070 // bare `list` the empty literal would otherwise yield (issue #5022).
1071 if (is_empty_list_literal(var_node))
1072 {
1073 std::string recovered = recover_list_type_from_appends(var_name);
1074 if (!recovered.empty())
1075 return recovered;
1076 }
1077
1078 std::string inferred_type = get_argument_type(var_node["value"]);
1079 if (!inferred_type.empty())
1080 return inferred_type;
1081 }
1082 }
1083 else if (arg["_type"] == "BinOp")
1084 {
1085 // For binary operations, try to infer from operands. Resolve them in the
1086 // enclosing function first, so a parameter is not typed by a module global
1087 // of the same name.
1088 Json dummy_stmt = {{"value", arg}};
1089 const Json func =
1090 json_utils::try_find_function(ast_["body"], get_current_func_name());
1091 return get_type_from_binary_expr(dummy_stmt, func.empty() ? ast_ : func);
1092 }
1093 else if (arg["_type"] == "List")
1094 {
1095 // Handle list literals like [-5, 1, 3, 5, 7, 10]
1096 return get_list_type_from_literal(arg);
1097 }
1098 else if (arg["_type"] == "Dict")
1099 {
1100 // Infer dict[K, V] from a literal so callers passing e.g. {"a": [3]}
1101 // give the parameter a fully parameterized annotation. Without the
1102 // value type, a nested value like list[int] cannot be resolved at the
1103 // subscript site and arithmetic on it crashes (#4830). Only upgrade to
1104 // the generic form when both key and value types are determined and
1105 // homogeneous; otherwise keep the bare "dict" fallback.
1106 // Dict unpacking ({**other}) yields a null entry in "keys"; such
1107 // entries are not inferable, so any of them forces the bare fallback.
1108 auto all_objects = [](const Json &arr) {
1109 for (const auto &e : arr)
1110 if (!e.is_object())
1111 return false;
1112 return true;
1113 };
1114
1115 if (
1116 arg.contains("keys") && arg["keys"].is_array() && !arg["keys"].empty() &&
1117 arg.contains("values") && arg["values"].is_array() &&
1118 !arg["values"].empty() && all_objects(arg["keys"]) &&
1119 all_objects(arg["values"]))
1120 {
1121 std::string key_type = get_argument_type(arg["keys"][0]);
1122 std::string value_type = get_argument_type(arg["values"][0]);
1123 if (!key_type.empty() && !value_type.empty())
1124 {
1125 bool homogeneous = true;
1126 for (size_t i = 1; i < arg["keys"].size(); ++i)
1127 if (get_argument_type(arg["keys"][i]) != key_type)
1128 {
1129 homogeneous = false;
1130 break;
1131 }
1132 for (size_t i = 1; homogeneous && i < arg["values"].size(); ++i)
1133 if (get_argument_type(arg["values"][i]) != value_type)
1134 {
1135 homogeneous = false;
1136 break;
1137 }
1138 if (homogeneous)
1139 return "dict[" + key_type + ", " + value_type + "]";
1140 }
1141 }
1142 return "dict";
1143 }
1144 else if (arg["_type"] == "Set")
1145 return "set";
1146 else if (arg["_type"] == "Tuple")
1147 {
1148 // Spell out the element types when every one of them is known. A bare
1149 // "tuple" leaves a parameter that receives this value unannotated, and
1150 // the converter then guesses it is a list (GitHub #7359).
1151 if (arg.contains("elts") && arg["elts"].is_array() && !arg["elts"].empty())
1152 {
1153 std::string params;
1154 for (const auto &elt : arg["elts"])
1155 {
1156 const std::string elt_type = get_argument_type(elt);
1157 if (elt_type.empty() || elt_type == "Any")
1158 {
1159 params.clear();
1160 break;
1161 }
1162 if (!params.empty())
1163 params += ", ";
1164 params += elt_type;
1165 }
1166 if (!params.empty())
1167 return "tuple[" + params + "]";
1168 }
1169 return "tuple";
1170 }
1171 else if (arg["_type"] == "BoolOp")
1172 {
1173 if (arg.contains("values") && arg["values"].is_array())
1174 {
1175 for (const auto &val : arg["values"])
1176 {
1177 if (
1178 val.contains("_type") && val["_type"] == "Call" &&
1179 val.contains("func") && val["func"].contains("_type") &&
1180 val["func"]["_type"] == "Name" && val["func"].contains("id"))
1181 {
1182 auto it = builtin_functions().find(val["func"]["id"]);
1183 if (it != builtin_functions().end())
1184 return it->second;
1185 }
1186 }
1187 }
1188 return "bool";
1189 }
1190 else if (arg["_type"] == "IfExp")
1191 {
1192 // Only literal arms: a name's inferred type can hide a run-time str.
1193 const std::function<bool(const Json &)> is_literal = [&](const Json &e) {
1194 const std::string kind = e.value("_type", "");
1195 return kind == "Constant" ||
1196 (kind == "IfExp" && is_literal(e["body"]) &&
1197 is_literal(e["orelse"]));
1198 };
1199 const std::string body_type = get_argument_type(arg["body"]);
1200 if (is_literal(arg) && body_type == get_argument_type(arg["orelse"]))
1201 return body_type;
1202 }
1203 else if (arg["_type"] == "Call")
1204 return get_call_type(arg);
1205 else if (arg["_type"] == "Attribute")
1206 return get_attribute_type(arg);
1207
1208 return ""; // Cannot determine type
1209}
1210
1214template <class Json>
1215std::string python_annotation<Json>::get_attribute_type(const Json &arg)
1216{
1217 const Json &obj = arg["value"];
1218 const std::string obj_kind = obj.value("_type", "");
1219 if (
1220 obj_kind != "Call" &&
1221 (obj_kind != "Name" ||
1222 is_rebound_in_scope(obj["id"].template get<std::string>())))
1223 return "";
1224
1225 const bool is_self =
1226 obj_kind == "Name" && obj["id"] == "self" && !current_class_name_.empty();
1227 return class_attribute_type(
1228 is_self ? current_class_name_ : get_argument_type(obj),
1229 arg["attr"].template get<std::string>(),
1230 is_self);
1231}
1232
1235template <class Json>
1236std::string python_annotation<Json>::class_attribute_type(
1237 const std::string &cls,
1238 const std::string &attr,
1239 bool constructed_only)
1240{
1241 for (const std::string &ancestor : get_class_ancestors(cls))
1242 {
1243 const Json class_node = json_utils::find_class(ast_["body"], ancestor);
1244 if (class_node.empty())
1245 return "";
1246 const Json annotation = json_utils::find_attr_annotation(class_node, attr);
1247 if (!annotation.empty())
1248 return constructed_only ? ""
1250 // An unannotated `self.attr = C()` types it as C only when nothing else
1251 // in the program stores to `.attr` on an instance that could be this one.
1252 const std::string constructed =
1253 json_utils::constructed_attr_class(class_node, attr);
1254 if (
1255 constructed.empty() ||
1256 json_utils::find_class(ast_["body"], constructed).empty() ||
1257 json_utils::count_attr_stores(ast_, attr, true) != 0)
1258 continue;
1259 const auto cls_ancestors = get_class_ancestors(cls);
1260 std::size_t self_stores = 0;
1261 for (const Json &node : ast_["body"])
1262 {
1263 if (node.value("_type", "") != "ClassDef")
1264 continue;
1265 const std::string name = node["name"].template get<std::string>();
1266 const auto ancestors = get_class_ancestors(name);
1267 const bool is_base =
1268 std::find(cls_ancestors.begin(), cls_ancestors.end(), name) !=
1269 cls_ancestors.end();
1270 const bool is_derived =
1271 std::find(ancestors.begin(), ancestors.end(), cls) != ancestors.end();
1272 if (is_base || is_derived)
1273 self_stores += json_utils::count_attr_stores(node, attr, false);
1274 }
1275 if (self_stores == 1)
1276 return constructed;
1277 }
1278 return "";
1279}
1280
1284template <class Json>
1286 const Json &node,
1287 const std::string &attr,
1288 const std::string &name,
1289 size_t &stores,
1290 size_t &name_reads)
1291{
1292 if (node.is_array())
1293 {
1294 for (const Json &child : node)
1295 count_attr_writers(child, attr, name, stores, name_reads);
1296 return;
1297 }
1298 if (!node.is_object())
1299 return;
1300 const std::string type = node.value("_type", "");
1301 const std::string ctx =
1302 node.contains("ctx") ? node["ctx"].value("_type", "") : "";
1303 if (
1304 (type == "Attribute" && node.value("attr", "") == attr && ctx != "Load") ||
1305 (type == "Attribute" && node.value("attr", "") == "__class__") ||
1306 (type == "Name" && node.value("id", "") == "setattr"))
1307 ++stores;
1308 if (type == "Name" && node.value("id", "") == name)
1309 ++name_reads;
1310 for (auto it = node.begin(); it != node.end(); ++it)
1311 if (it.key() != "annotation" && it.key() != "returns")
1312 count_attr_writers(it.value(), attr, name, stores, name_reads);
1313}
1314
1320template <class Json>
1321std::string python_annotation<Json>::self_attribute_type(const Json &node)
1322{
1323 if (
1324 node.value("_type", "") != "Attribute" ||
1325 node["value"].value("_type", "") != "Name" ||
1326 node["value"].value("id", "") != "self" || current_class_name_.empty() ||
1327 is_rebound_in_scope("self"))
1328 return "";
1329 const std::string attr = node["attr"].template get<std::string>();
1330 for (const std::string &cls : get_class_ancestors(current_class_name_))
1331 {
1332 const Json class_node = json_utils::find_class(ast_["body"], cls);
1333 if (class_node.empty())
1334 return "";
1336 cls, attr, ast_["body"]);
1337 if (value.empty())
1338 continue;
1339 size_t stores = 0;
1340 size_t name_reads = 0;
1341 count_attr_writers(ast_["body"], attr, cls, stores, name_reads);
1342 const Json annotation = json_utils::find_attr_annotation(class_node, attr);
1343 const std::vector<Json> calls = find_function_calls(cls, ast_["body"]);
1344 if (stores != 1 || annotation.empty() || calls.size() != name_reads)
1345 return "";
1346 const std::string type = json_utils::render_annotation_type(annotation);
1347 if (value.value("_type", "") == "Constant")
1348 return get_argument_type(value) == type ? type : "";
1349 if (value.value("_type", "") != "Name")
1350 return "";
1351
1352 const Json init =
1353 json_utils::try_find_function(class_node["body"], "__init__");
1354 const Json &params = init["args"]["args"];
1355 size_t index = 0;
1356 while (index < params.size() && params[index]["arg"] != value["id"])
1357 ++index;
1358 if (index == 0 || index == params.size())
1359 return "";
1360 for (const Json &call : calls)
1361 {
1362 if (
1363 !call["keywords"].empty() || call["args"].size() < index ||
1364 call["args"][index - 1].value("_type", "") != "Constant" ||
1365 get_argument_type(call["args"][index - 1]) != type)
1366 return "";
1367 for (const Json &arg : call["args"])
1368 if (arg.value("_type", "") == "Starred")
1369 return "";
1370 }
1371 return type;
1372 }
1373 return "";
1374}
1375
1377template <class Json>
1378std::string python_annotation<Json>::get_operand_type(const Json &node)
1379{
1380 const std::string type = get_argument_type(node);
1381 return type.empty() ? self_attribute_type(node) : type;
1382}
1383
1384// Recursively search a statement subtree for the first
1385// ``<name>.append(arg)`` call and return its argument node. Returns an
1386// empty Json when no such append exists. Used to recover the element
1387// type of an empty list literal (issue #3239).
1388template <class Json>
1389static Json find_first_append_arg(const Json &node, const std::string &name)
1390{
1391 if (node.is_object())
1392 {
1393 if (
1394 node.value("_type", std::string()) == "Call" && node.contains("func") &&
1395 node["func"].is_object() &&
1396 node["func"].value("_type", std::string()) == "Attribute" &&
1397 node["func"].value("attr", std::string()) == "append" &&
1398 node["func"].contains("value") && node["func"]["value"].is_object() &&
1399 node["func"]["value"].value("_type", std::string()) == "Name" &&
1400 node["func"]["value"].value("id", std::string()) == name &&
1401 node.contains("args") && node["args"].is_array() &&
1402 node["args"].size() == 1)
1403 {
1404 return node["args"][0];
1405 }
1406 for (auto it = node.begin(); it != node.end(); ++it)
1407 {
1408 Json found = find_first_append_arg(*it, name);
1409 if (!found.empty())
1410 return found;
1411 }
1412 }
1413 else if (node.is_array())
1414 {
1415 for (const auto &child : node)
1416 {
1417 Json found = find_first_append_arg(child, name);
1418 if (!found.empty())
1419 return found;
1420 }
1421 }
1422 return Json();
1423}
1424
1425// Method to get the full type of a list literal
1426template <class Json>
1427std::string
1428python_annotation<Json>::get_list_type_from_literal(const Json &list_arg)
1429{
1430 if (!list_arg.contains("elts") || list_arg["elts"].empty())
1431 {
1433 "Empty or malformed list literal detected. Using 'list[int]' as "
1434 "default ({}:{})",
1435 python_filename_,
1436 current_line_);
1437 return "list"; // Default fallback
1438 }
1439
1440 // Get the type of the first element
1441 std::string element_type = get_argument_type(list_arg["elts"][0]);
1442
1443 // An inner list left untyped above leaves the outer one to its call sites
1444 if (element_type.empty() && list_arg["elts"][0].value("_type", "") == "List")
1445 return "";
1446
1447 if (element_type.empty())
1448 {
1450 "Could not determine list element type. Using 'list[int]' as fallback "
1451 "({}:{})",
1452 python_filename_,
1453 current_line_);
1454 return "list[int]"; // Fallback for numeric contexts
1455 }
1456
1457 // Check if all elements have the same type, tracking numeric promotion.
1458 // In Python an int/float mix promotes to float (e.g. [4.0, 3] is
1459 // list[float]), so a single leading int must not truncate the float elements.
1460 auto is_numeric = [](const std::string &t) {
1461 return t == "int" || t == "float" || t == "bool";
1462 };
1463 bool all_numeric = is_numeric(element_type);
1464 bool saw_float = element_type == "float";
1465 bool mixed = false;
1466
1467 for (size_t i = 1; i < list_arg["elts"].size(); ++i)
1468 {
1469 std::string current_type = get_argument_type(list_arg["elts"][i]);
1470 if (current_type.empty())
1471 continue;
1472 if (current_type == "float")
1473 saw_float = true;
1474 if (!is_numeric(current_type))
1475 all_numeric = false;
1476
1477 if (current_type != element_type)
1478 {
1479 mixed = true;
1480 // Tuples of differing shape have no common element type: unlike a list
1481 // element, a tuple parameter is a concrete struct, so neither widening
1482 // the spelling nor the int fallback below would match what the caller
1483 // passes. Report no inference instead (GitHub #7359).
1484 if (
1485 element_type.rfind("tuple", 0) == 0 ||
1486 current_type.rfind("tuple", 0) == 0)
1487 return "";
1488 // Nor does a list element beside a scalar, or a str beside another
1489 // type: claiming list[int] reads the inner list or the str as an int
1490 // (#8472, #8670). The converter types such a parameter from its call
1491 // sites instead.
1492 if (
1493 (element_type.rfind("list", 0) == 0) !=
1494 (current_type.rfind("list", 0) == 0) ||
1495 (element_type == "str") != (current_type == "str"))
1496 return "";
1497 // Other non-numeric heterogeneity (e.g. None vs int) has no common
1498 // element type we can represent, so keep the historical int fallback.
1499 if (!all_numeric)
1500 {
1502 "Mixed types detected in list literal: {} vs {}. Using 'list[int]' "
1503 "as fallback ({}:{})",
1504 element_type,
1505 current_type,
1506 python_filename_,
1507 current_line_);
1508 return "list[int]";
1509 }
1510 }
1511 }
1512
1513 // All elements numeric: promote an int/float mix to float (Python widens int
1514 // to float). Any other numeric mix (e.g. int/bool) keeps the int fallback.
1515 if (mixed && all_numeric)
1516 return saw_float ? "list[float]" : "list[int]";
1517
1518 // Return the full generic type notation
1519 return "list[" + element_type + "]";
1520}
1521
1525template <class Json>
1526std::string python_annotation<Json>::bare_list_subscript_type(
1527 const Json &var_node,
1528 const std::string &var_name,
1529 bool is_slice)
1530{
1531 if (!is_empty_list_literal(var_node))
1532 return homogeneous_literal_subscript_type(var_node, var_name, is_slice);
1533 const std::string elem =
1534 subscript_result_type(recover_list_type_from_appends(var_name), is_slice);
1535 return elem.empty() ? "Any" : elem;
1536}
1537
1540template <class Json>
1541static bool name_only_indexed(const Json &node, const std::string &name)
1542{
1543 auto is_load = [](const Json &n) {
1544 return n.contains("ctx") && n["ctx"].value("_type", "") == "Load";
1545 };
1546 if (node.is_object())
1547 {
1548 const std::string kind = node.value("_type", "");
1549 if (kind == "Name" && node.value("id", "") == name)
1550 return !is_load(node);
1551 if (
1552 kind == "Subscript" && node["value"].value("_type", "") == "Name" &&
1553 node["value"].value("id", "") == name)
1554 return is_load(node) && name_only_indexed(node["slice"], name);
1555 }
1556 for (const Json &child : node)
1557 if (child.is_structured() && !name_only_indexed(child, name))
1558 return false;
1559 return true;
1560}
1561
1566template <class Json>
1567std::string python_annotation<Json>::homogeneous_literal_subscript_type(
1568 const Json &var_node,
1569 const std::string &var_name,
1570 bool is_slice)
1571{
1572 static const std::set<std::string> scalars = {"bool", "int", "float", "str"};
1573 const Json value = var_node.value("value", Json());
1574 if (
1575 !value.is_object() || value.value("_type", "") != "List" ||
1576 json_utils::count_name_bindings(ast_, var_name) != 1 ||
1577 !name_only_indexed(ast_, var_name))
1578 return "Any";
1579 std::string elem;
1580 for (const Json &e : value["elts"])
1581 {
1582 const std::string t = get_argument_type(e);
1583 if (scalars.count(t) == 0 || (!elem.empty() && t != elem))
1584 return "Any";
1585 elem = t;
1586 }
1587 return is_slice ? "list[" + elem + "]" : elem;
1588}
1589
1592template <class Json>
1593std::string python_annotation<Json>::refine_any_from_value(
1594 const std::string &annotated,
1595 const Json &var_node)
1596{
1597 if (
1598 annotated != "Any" || !var_node.contains("value") ||
1599 var_node["value"].is_null())
1600 return annotated;
1601 const std::string value_type = get_argument_type(var_node["value"]);
1602 return value_type.empty() ? annotated : value_type;
1603}
1604
1605template <class Json>
1606std::string python_annotation<Json>::recover_list_type_from_appends(
1607 const std::string &var_name)
1608{
1609 // Look for `<var_name>.append(arg)` inside the enclosing function. The
1610 // comprehension lowering emits the empty-list init and its appends in the
1611 // same function body, so prefer that scope; fall back to the module body
1612 // for module-level comprehensions (where no current function name is set).
1613 const std::string &scope = get_current_func_name();
1614 const Json &module_body = ast_["body"];
1615 Json func =
1616 scope.empty() ? Json() : json_utils::try_find_function(module_body, scope);
1617 const Json &body =
1618 (!func.empty() && func.contains("body")) ? func["body"] : module_body;
1619
1620 Json append_arg = find_first_append_arg(body, var_name);
1621 if (append_arg.empty())
1622 return "";
1623
1624 // Only commit to a parameterised list when the element type resolves to a
1625 // concrete type. An unresolved ("") or dynamic ("Any") element keeps the
1626 // permissive bare `list` so heterogeneous/object lists are unaffected
1627 // (mirrors the jpl_1 / #3239 guard in infer_type).
1628 std::string elem_type = get_argument_type(append_arg);
1629 if (elem_type.empty() || elem_type == "Any")
1630 return "";
1631
1632 return "list[" + elem_type + "]";
1633}
1634
1635template <class Json>
1636std::string python_annotation<Json>::arg_dependent_call_type(const Json &call)
1637{
1638 const std::string &func_name = call["func"]["id"];
1639 // hash() returns bytes only for the consensus spec's own hash(data: bytes)
1640 // (mirrors get_argument_type).
1641 if (
1642 func_name == "hash" && call.contains("args") && !call["args"].empty() &&
1643 get_argument_type(call["args"][0]) != "bytes")
1644 return "int";
1645 return func_name == "abs" ? get_builtin_call_type(call) : "";
1646}
1647
1648template <class Json>
1649std::string python_annotation<Json>::get_builtin_call_type(const Json &call)
1650{
1651 const std::string &func_name = call["func"]["id"];
1652 // abs() of a float or a complex is a float; the table's int covers the rest.
1653 if (func_name == "abs" && call.contains("args") && call["args"].size() == 1)
1654 {
1655 const std::string arg_type = get_argument_type(call["args"][0]);
1656 if (arg_type == "float" || arg_type == "complex")
1657 return "float";
1658 }
1659 auto it = builtin_functions().find(func_name);
1660 return it != builtin_functions().end() ? it->second : func_name;
1661}
1662
1667template <class Json>
1668std::string python_annotation<Json>::list_concat_type(const Json &binop)
1669{
1670 const Json &rhs = binop["right"];
1671 if (binop["op"]["_type"] != "Add")
1672 return "list";
1673 const std::string callee =
1674 rhs["_type"] == "Call" ? rhs["func"].value("id", "") : "";
1675 const std::string builder_elem =
1676 callee.rfind("_nondet_list_", 0) == 0 ? callee.substr(13) : "";
1677 std::string right = "list";
1678 if (rhs["_type"] == "List")
1679 right = get_list_type_from_literal(rhs);
1680 else if (
1681 builder_elem == "int" || builder_elem == "float" ||
1682 builder_elem == "bool" || builder_elem == "str")
1683 right = "list[" + builder_elem + "]";
1684 const std::string left = get_list_type_from_literal(binop["left"]);
1685 return left == right ? left : "list";
1686}
1687
1688template <class Json>
1689std::string python_annotation<Json>::get_type_from_binary_expr(
1690 const Json &stmt,
1691 const Json &body)
1692{
1693 std::string type("");
1694
1695 const Json &lhs =
1696 stmt.contains("value") ? stmt["value"]["left"] : stmt["left"];
1697
1698 // True division (`/`) by a float literal yields float regardless of the
1699 // LHS arithmetic operands, covering the `a * h / 2.0` shape that
1700 // otherwise truncates the call-site return to long_long_int. Limited to
1701 // a numeric LHS (Name/BinOp/UnaryOp/int-Constant) so a complex-typed
1702 // dividend like `complex(6.0, 4.0) / 2.0` falls through to the LHS-type
1703 // path below, which correctly preserves "complex".
1704 if (
1705 stmt.contains("value") && stmt["value"].contains("op") &&
1706 stmt["value"]["op"].contains("_type") &&
1707 stmt["value"]["op"]["_type"] == "Div")
1708 {
1709 const auto &rhs = stmt["value"]["right"];
1710 bool rhs_is_float_literal =
1711 rhs.contains("_type") && rhs["_type"] == "Constant" &&
1712 rhs.contains("value") && rhs["value"].is_number_float();
1713 bool lhs_is_numeric_shape =
1714 lhs.contains("_type") &&
1715 (lhs["_type"] == "Name" || lhs["_type"] == "BinOp" ||
1716 lhs["_type"] == "UnaryOp" ||
1717 (lhs["_type"] == "Constant" && lhs.contains("value") &&
1718 (lhs["value"].is_number_integer() || lhs["value"].is_number_float())));
1719 if (
1720 rhs_is_float_literal && lhs_is_numeric_shape &&
1721 !is_array_operand(lhs, body))
1722 return "float";
1723 }
1724
1725 // Python numeric-tower promotion: an arithmetic operation with a complex
1726 // operand yields complex (e.g. `3 + 4j`). The LHS-driven inference below only
1727 // detects a complex LHS (so `4j + 3` already works); a complex RHS would
1728 // otherwise be ignored and the result mis-typed as the LHS's int/float,
1729 // breaking later `.real`/`.imag` access on the assigned variable.
1730 {
1731 const Json &rhs =
1732 stmt.contains("value") ? stmt["value"]["right"] : stmt["right"];
1733 auto operand_is_complex = [&](const Json &n) -> bool {
1734 if (!n.is_object() || !n.contains("_type"))
1735 return false;
1736 if (n["_type"] == "Constant")
1737 return get_type_from_constant(n) == "complex";
1738 // complex(...) constructor call
1739 return n["_type"] == "Call" && n.contains("func") &&
1740 n["func"].is_object() && n["func"].contains("_type") &&
1741 n["func"]["_type"] == "Name" && n["func"].contains("id") &&
1742 n["func"]["id"] == "complex";
1743 };
1744 if (operand_is_complex(lhs) || operand_is_complex(rhs))
1745 return "complex";
1746 // A number on the left of an array (`2 * a`) gives an array.
1747 if (!is_array_operand(lhs, body) && is_array_operand(rhs, body))
1748 return "list";
1749 // Only a list concatenates or repeats with a list (`a + [x]`, `n * [x]`);
1750 // a list literal on the left keeps its element type below.
1751 const Json &value = stmt.contains("value") ? stmt["value"] : stmt;
1752 const std::string op = value["op"].value("_type", "");
1753 if (
1754 (op == "Add" || op == "Mult") && lhs["_type"] != "List" &&
1755 (rhs["_type"] == "List" || rhs["_type"] == "ListComp"))
1756 return "list";
1757 }
1758
1759 if (lhs["_type"] == "BinOp")
1760 type = get_type_from_binary_expr(lhs, body);
1761 else if (lhs["_type"] == "List")
1762 type = list_concat_type(stmt.contains("value") ? stmt["value"] : stmt);
1763 else if (lhs["_type"] == "Tuple")
1764 type = "tuple";
1765 // Floor division (//) operations always result in an integer value
1766 else if (
1767 stmt.contains("value") && stmt["value"]["op"]["_type"] == "FloorDiv" &&
1768 !is_array_operand(lhs, body))
1769 type = "int";
1770 else
1771 {
1772 // If the LHS of the binary operation is a variable, its type is retrieved
1773 if (lhs["_type"] == "Name")
1774 {
1775 // Retrieve the type from the variable declaration within the current
1776 // function body
1777 Json left_op = find_annotated_assign(lhs["id"], body["body"]);
1778
1779 // If not found in the function body, try to retrieve it from the function
1780 // arguments
1781 if (left_op.empty() && body.contains("args"))
1782 left_op = find_annotated_assign(lhs["id"], body["args"]["args"]);
1783
1784 // Check current function scope for function parameters
1785 if (
1786 left_op.empty() && current_func != nullptr &&
1787 (*current_func).contains("args"))
1788 {
1789 left_op =
1790 find_annotated_assign(lhs["id"], (*current_func)["args"]["args"]);
1791 }
1792
1793 type = operand_annotation_type(left_op);
1794 // As a fallback, check global scope for prior annotation
1795 if (type.empty())
1796 type = operand_annotation_type(
1797 find_annotated_assign(lhs["id"], ast_["body"]));
1798 }
1799 else if (lhs["_type"] == "UnaryOp")
1800 {
1801 const auto &operand = lhs["operand"];
1802 if (operand["_type"] == "Constant")
1803 type = get_type_from_constant(operand);
1804 }
1805 else if (lhs["_type"] == "Subscript")
1806 {
1807 // Handle subscript operations like dp[i-1], prices[i], M[0][0], etc.
1808 // Delegate to the nested-aware resolver so multi-level subscripts
1809 // (e.g. M[0][0], whose value is itself a Subscript rather than a Name)
1810 // do not blindly dereference lhs["value"]["id"] (Fixes #5122).
1811 type = resolve_subscript_type(lhs, body);
1812 }
1813 else if (lhs["_type"] == "Call" && lhs["func"]["_type"] == "Name")
1814 {
1815 // Handle function calls in binary expressions like float("1.1") +
1816 // float("2.2")
1817 const std::string &func_name = lhs["func"]["id"];
1818
1819 if (type_utils::is_builtin_type(func_name))
1820 type = get_builtin_call_type(lhs);
1821 else
1822 type = get_function_return_type(
1823 func_name, body); // For user-defined functions
1824 }
1825 else if (lhs["_type"] == "Constant")
1826 type = get_type_from_constant(lhs);
1827 else if (lhs["_type"] == "Call" && lhs["func"]["_type"] == "Attribute")
1828 type = get_type_from_method(lhs);
1829 else if (lhs["_type"] == "Attribute")
1830 {
1831 // Construct full attribute name (e.g., "string.digits")
1832 if (lhs["value"]["_type"] == "Name" && lhs["value"].contains("id"))
1833 {
1834 std::string full_name = lhs["value"]["id"].template get<std::string>() +
1835 "." + lhs["attr"].template get<std::string>();
1836 auto it = builtin_functions().find(full_name);
1837 if (it != builtin_functions().end())
1838 type = it->second;
1839 }
1840 // The result is typed from the left operand: only when the right one
1841 // is a literal or another such attribute, and a number for a number
1842 // (not a float that would be truncated), or an int or the same type
1843 // for a str or list. A negative exponent
1844 // gives a float, and arithmetic on a bool an int.
1845 const Json &value = stmt.contains("value") ? stmt["value"] : stmt;
1846 const std::string attr_type =
1847 type.empty() ? self_attribute_type(lhs) : std::string();
1848 const Json &rhs = value["right"];
1849 const std::string rhs_type = rhs.value("_type", "") == "Constant"
1850 ? get_argument_type(rhs)
1851 : self_attribute_type(rhs);
1852 const bool numeric = attr_type == "int" || attr_type == "float";
1853 const bool rhs_fits =
1854 numeric ? rhs_type == "int" || rhs_type == "float" || rhs_type == "bool"
1855 : rhs_type == "int" || rhs_type == attr_type;
1856 if (
1857 !attr_type.empty() && attr_type != "bool" && rhs_fits &&
1858 value["op"].value("_type", "") != "Pow")
1859 type = attr_type;
1860 }
1861 }
1862
1863 // If still unknown, try RHS or fallback to Any for arithmetic ops
1864 if (type.empty())
1865 {
1866 const Json &rhs =
1867 stmt.contains("value") ? stmt["value"]["right"] : stmt["right"];
1868
1869 if (rhs["_type"] == "Constant")
1870 type = get_type_from_constant(rhs);
1871 else if (rhs["_type"] == "Name")
1872 type = operand_annotation_type(find_annotated_assign(
1873 rhs["id"], body.contains("body") ? body["body"] : ast_["body"]));
1874
1875 if (
1876 type.empty() && stmt.contains("value") && stmt["value"].contains("op") &&
1877 stmt["value"]["op"].contains("_type"))
1878 {
1879 type = "Any";
1880 }
1881 }
1882
1883 return promote_to_float_operand(stmt, type);
1884}
1885
1889template <class Json>
1890std::string python_annotation<Json>::operand_annotation_type(const Json &decl)
1891{
1892 if (decl.empty() || !decl.contains("annotation"))
1893 return "";
1894 const Json &annotation = decl["annotation"];
1895 if (annotation.contains("id") && annotation["id"].is_string())
1896 return annotation["id"];
1897 const std::string base = json_utils::get_annotation_type_name(annotation);
1898 const std::string base_name = base.substr(0, base.find('['));
1899 return base_name == "list" || base_name == "tuple" ? base_name : "";
1900}
1901
1904template <class Json>
1905bool python_annotation<Json>::is_array_operand(
1906 const Json &operand,
1907 const Json &body)
1908{
1909 const std::string type = operand.value("_type", std::string());
1910 if (type == "Call")
1911 return is_numpy_array_ctor_call(operand) ||
1913 static_cast<const nlohmann::json &>(operand),
1914 static_cast<const nlohmann::json &>(ast_))
1915 .empty();
1916 if (type != "Name")
1917 return false;
1918 const std::string name = operand["id"].template get<std::string>();
1919 Json decl = find_annotated_assign(
1920 name, body.contains("body") ? body["body"] : ast_["body"]);
1921 if (decl.empty())
1922 decl = find_annotated_assign(name, ast_["body"]);
1923 return !decl.empty() && decl.contains("annotation") &&
1924 decl["annotation"].value("id", std::string()) == "list";
1925}
1926
1930template <class Json>
1932 const Json &stmt,
1933 const std::string &type)
1934{
1935 static const std::set<std::string> arithmetic{
1936 "Add", "Sub", "Mult", "FloorDiv", "Mod", "Pow"};
1937 const Json &expr = stmt.contains("value") ? stmt["value"] : stmt;
1938 if (
1939 (type != "int" && type != "bool") || !expr.is_object() ||
1940 !expr.contains("op") || !expr["op"].is_object())
1941 return type;
1942 const std::string op = expr["op"].value("_type", "");
1943 if (op == "Div")
1944 return "float";
1945 return arithmetic.count(op) != 0 && get_operand_type(expr["right"]) == "float"
1946 ? "float"
1947 : type;
1948}
1949
1950template <class Json>
1951std::string
1952python_annotation<Json>::infer_lambda_return_type(const Json &lambda_elem) const
1953{
1954 const Json &lambda_body = lambda_elem["value"]["body"];
1955 if (lambda_body["_type"] == "BinOp")
1956 return "float"; // Match converter's default of double_type()
1957 else if (lambda_body["_type"] == "Compare")
1958 return "bool";
1959 else
1960 return "Any"; // Default for other lambda expressions
1961}
1962
1964template <class Json>
1965static bool union_has_none(const Json &slice)
1966{
1967 if (!slice.is_object() || !slice.contains("elts"))
1968 return false;
1969 for (const auto &elt : slice["elts"])
1970 if (elt.value("_type", "") == "Constant" && elt["value"].is_null())
1971 return true;
1972 return false;
1973}
1974
1975template <class Json>
1976std::string python_annotation<Json>::get_function_return_type(
1977 const std::string &func_name,
1978 const Json &ast)
1979{
1980 // Guard against infinite recursion
1981 if (functions_in_analysis_.count(func_name) > 0)
1982 {
1983 // Re-entering this branch means the self-reference is indirect, as in
1984 // `r = f(n - 1); return r`, where the returned name's binding calls f.
1985 // expr_calls_function only scans the return expression, so it does not
1986 // skip that return, and re-running inference here would not terminate.
1987 // The base-case returns supply the type.
1988 if (functions_reentered_.count(func_name) > 0)
1989 return "";
1990 struct reentry_scope
1991 {
1992 std::set<std::string> &set;
1993 const std::string &name;
1994 ~reentry_scope()
1995 {
1996 set.erase(name);
1997 }
1998 } scope{functions_reentered_, func_name};
1999 functions_reentered_.insert(func_name);
2000
2001 // Function is calling itself: try to infer from non-recursive return
2002 // statements
2003 for (const Json &elem : ast["body"])
2004 {
2005 if (elem["_type"] == "FunctionDef" && elem["name"] == func_name)
2006 {
2007 // When override flag is set, always infer instead of using annotation
2008 bool should_override =
2009 config.options.get_bool_option("override-return-annotation");
2010
2011 // Check if function has explicit return type annotation
2012 if (
2013 !should_override && elem.contains("returns") &&
2014 !elem["returns"].is_null() && elem["returns"].contains("id"))
2015 return elem["returns"]["id"];
2016
2017 // Try to infer from return statements (excluding recursive calls).
2018 // Resolve names in func_name's own scope (see the non-recursive path
2019 // below; GitHub #5104, #5105).
2020 std::string saved_rec_ctx = current_func_name_context_;
2021 current_func_name_context_ = func_name;
2022 std::string inferred =
2023 infer_from_return_statements(elem["body"], func_name);
2024 current_func_name_context_ = saved_rec_ctx;
2025 return inferred;
2026 }
2027 }
2028 return "";
2029 }
2030
2031 // Add function to set before analysis
2032 functions_in_analysis_.insert(func_name);
2033
2034 // Get type from nondet_<type> functions
2035 if (func_name.find("nondet_") == 0)
2036 {
2037 size_t last_underscore_pos = func_name.find_last_of('_');
2038 if (last_underscore_pos != std::string::npos)
2039 {
2040 functions_in_analysis_.erase(func_name);
2041 return func_name.substr(last_underscore_pos + 1);
2042 }
2043 }
2044
2045 // Check override flag
2046 bool should_override =
2047 config.options.get_bool_option("override-return-annotation");
2048
2049 // Search for function (including nested functions) using recursive helper
2050 Json func_elem = find_function_recursive(func_name, ast_["body"]);
2051
2052 // Also check current function scope for local nested functions
2053 if (func_elem.empty() && current_func != nullptr)
2054 {
2055 func_elem = find_function_recursive(func_name, (*current_func)["body"]);
2056 }
2057
2058 // Process the found function (if any)
2059 if (!func_elem.empty())
2060 {
2061 // If override is set, skip annotation and go straight to inference
2062 if (!should_override)
2063 {
2064 // Check if function has a return type annotation first (before inferring)
2065 if (func_elem.contains("returns") && !func_elem["returns"].is_null())
2066 {
2067 const auto &returns = func_elem["returns"];
2068
2069 // Handle different return type annotation structures
2070 if (returns.contains("_type"))
2071 {
2072 const std::string &return_type = returns["_type"];
2073
2074 // Handle Subscript type (e.g., List[int], Dict[str, int])
2075 if (return_type == "Subscript")
2076 {
2077 functions_in_analysis_.erase(func_name);
2078 // Bare "Optional" cannot hold None; leave the target unannotated
2079 // so it takes the callee's resolved type (#8016). Union[T, None]
2080 // is the same type.
2081 if (
2082 returns.contains("value") && returns["value"].contains("id") &&
2083 (returns["value"]["id"] == "Optional" ||
2084 (returns["value"]["id"] == "Union" &&
2085 union_has_none(returns.value("slice", Json())))))
2086 return "";
2087 if (returns.contains("value") && returns["value"].contains("id"))
2088 return returns["value"]["id"];
2089 else
2090 return "Any"; // Default for complex subscript types
2091 }
2092 // Handle Constant type (e.g., None)
2093 else if (return_type == "Constant")
2094 {
2095 functions_in_analysis_.erase(func_name);
2096 if (returns.contains("value") && returns["value"].is_null())
2097 return "NoneType";
2098 else if (returns.contains("value") && returns["value"].is_string())
2099 {
2100 // Forward reference annotation: -> "float", -> "MyClass", etc.
2101 // Validate that the string looks like a Python identifier (or
2102 // dotted name) before returning it, to guard against arbitrary
2103 // string constants used as non-type annotations.
2104 std::string type_name =
2105 returns["value"].template get<std::string>();
2106 bool valid = !type_name.empty() &&
2107 (std::isalpha((unsigned char)type_name[0]) ||
2108 type_name[0] == '_');
2109 for (size_t i = 1; valid && i < type_name.size(); ++i)
2110 valid = std::isalnum((unsigned char)type_name[i]) ||
2111 type_name[i] == '_' || type_name[i] == '.';
2112 if (valid)
2113 return type_name;
2114 // Not a valid identifier — treat as opaque
2115 return "Any";
2116 }
2117 else if (returns.contains("value"))
2118 return "Any"; // Other constant types
2119 }
2120 // Handle other annotation types
2121 else
2122 {
2123 // Try to extract id if it exists
2124 if (returns.contains("id"))
2125 {
2126 // If the body also has `return None` paths, the function
2127 // actually returns Optional[T] regardless of the annotation.
2128 // Return "" so the converter handles this as Optional.
2129 if (has_return_none(func_elem["body"]))
2130 {
2131 functions_in_analysis_.erase(func_name);
2132 return "";
2133 }
2134 functions_in_analysis_.erase(func_name);
2135 return returns["id"];
2136 }
2137 }
2138 }
2139 // Handle case where returns exists but doesn't have expected structure
2140 else
2141 {
2143 "Unrecognized return type annotation for function "
2144 "{}",
2145 func_name);
2146 functions_in_analysis_.erase(func_name);
2147 return "Any"; // Safe default
2148 }
2149 }
2150 }
2151
2152 // Try to infer return type from actual return statements
2153 // Use recursive inference to find return types in all blocks.
2154 // Names in a return expression resolve in THIS function's scope, not the
2155 // caller's. Switch the inference context to func_name so a local variable
2156 // referenced by a return statement (e.g. `return v` where
2157 // `v = nondet_float()`) is found; otherwise the type cannot be inferred
2158 // and the parameter/return default to a pointer, crashing the FP backend
2159 // (GitHub #5104, #5105).
2160 std::string saved_return_ctx = current_func_name_context_;
2161 current_func_name_context_ = func_name;
2162 bool divergent = false;
2163 std::string inferred_type =
2164 infer_from_return_statements(func_elem["body"], func_name, &divergent);
2165 const std::string optional_return =
2166 returned_optional_scalar(func_elem, func_elem["body"]);
2167 current_func_name_context_ = saved_return_ctx;
2168
2169 if (optional_return == "Optional[" + inferred_type + "]")
2170 {
2171 functions_in_analysis_.erase(func_name);
2172 return optional_return;
2173 }
2174
2175 if (!inferred_type.empty() && inferred_type != "NoneType")
2176 {
2177 // Found a non-None return type; check for mixed value+None returns
2178 if (has_return_none(func_elem["body"]))
2179 {
2180 // Mixed returns: leave unannotated so converter handles via Optional
2181 functions_in_analysis_.erase(func_name);
2182 return "";
2183 }
2184 functions_in_analysis_.erase(func_name);
2185 return inferred_type;
2186 }
2187
2188 // Check top-level returns as fallback
2189 auto return_node = json_utils::find_return_node(func_elem["body"]);
2190 if (!return_node.empty())
2191 {
2192 // Branches returning incompatible types (e.g. int and str) have no
2193 // single type; narrowing to the first top-level return would mistype
2194 // the other path (#8890).
2195 if (divergent)
2196 {
2197 functions_in_analysis_.erase(func_name);
2198 return "";
2199 }
2200
2201 std::string fallback_type;
2202 std::string saved_fallback_ctx = current_func_name_context_;
2203 current_func_name_context_ = func_name;
2204 try
2205 {
2206 infer_type(return_node, func_elem, fallback_type);
2207 }
2208 catch (std::runtime_error &)
2209 {
2210 // Return value type could not be inferred (e.g. call through a
2211 // function-pointer parameter); leave fallback_type empty.
2212 }
2213 current_func_name_context_ = saved_fallback_ctx;
2214 functions_in_analysis_.erase(func_name);
2215 // An Any fallback says nothing, yet a caller returning this call would
2216 // be annotated Any and converted with an int return (#8358)
2217 return fallback_type == "Any" ? "" : fallback_type;
2218 }
2219
2220 // If function has no explicit return statements, assume void/None
2221 functions_in_analysis_.erase(func_name);
2222 return "NoneType";
2223 }
2224
2225 // Check for lambda assignments when regular function not found
2226 Json lambda_elem = find_lambda_in_body(func_name, ast["body"]);
2227
2228 // Also check current function scope if not found globally
2229 if (lambda_elem.empty() && current_func != nullptr)
2230 lambda_elem = find_lambda_in_body(func_name, (*current_func)["body"]);
2231
2232 if (!lambda_elem.empty())
2233 {
2234 functions_in_analysis_.erase(func_name);
2235 return infer_lambda_return_type(lambda_elem);
2236 }
2237
2238 // Get type from imported functions
2239 try
2240 {
2241 if (module_manager_)
2242 {
2243 const auto &import_node =
2244 json_utils::find_imported_function(ast_, func_name);
2245 // Relative imports (`from . import helper`) carry a null module name;
2246 // fall through to the wildcard/builtin probes instead of feeding null
2247 // to get_module (which would raise an uncatchable nlohmann type_error).
2248 if (!import_node.contains("module") || import_node["module"].is_null())
2249 throw std::runtime_error("import has no resolvable module name");
2250 auto module = module_manager_->get_module(import_node["module"]);
2251
2252 if (!module)
2253 throw std::runtime_error("module not found");
2254
2255 // Try to get it as a function first
2256 try
2257 {
2258 auto func_info = module->get_function(func_name);
2259
2260 // If return_type is empty or "NoneType", check if it's actually a class
2261 if (
2262 func_info.return_type_.empty() ||
2263 func_info.return_type_ == "NoneType")
2264 {
2265 // A class constructor (__init__ returns None) gives the class; a
2266 // function of unknown return type is no type at all (#8980).
2267 functions_in_analysis_.erase(func_name);
2268 return module->get_class(func_name).name_.empty()
2269 ? func_info.return_type_
2270 : func_name;
2271 }
2272
2273 functions_in_analysis_.erase(func_name);
2274 return func_info.return_type_;
2275 }
2276 catch (std::runtime_error &)
2277 {
2278 // If get_function fails, it might be a class
2279 functions_in_analysis_.erase(func_name);
2280 return func_name;
2281 }
2282 }
2283 }
2284 catch (std::runtime_error &)
2285 {
2286 }
2287
2288 // Probe wildcard imports (`from X import *`) for @p func_name when the
2289 // named-import lookup above missed. find_imported_function only matches
2290 // explicit aliases, so star-imported functions stay invisible to the
2291 // annotator's type inference without this fallback (GitHub #4564).
2292 if (std::string t = resolve_wildcard_import_func(func_name); !t.empty())
2293 {
2294 functions_in_analysis_.erase(func_name);
2295 return t;
2296 }
2297
2298 // Check if the function is a built-in function
2299 auto it = builtin_functions().find(func_name);
2300 if (it != builtin_functions().end())
2301 {
2302 functions_in_analysis_.erase(func_name);
2303 return it->second;
2304 }
2305
2306 // Check if the name is a class constructor
2307 // (e.g., A() returns an A instance)
2308 {
2309 const std::string resolved = json_utils::get_object_alias(ast_, func_name);
2310 if (json_utils::is_class(resolved, ast_))
2311 {
2312 functions_in_analysis_.erase(func_name);
2313 return resolved;
2314 }
2315 }
2316
2317 // `Alias = bytes`-style constructor call (e.g. an unannotated
2318 // `x = Alias(3)`): resolve to the aliased builtin's name.
2319 {
2320 const Json &decl =
2321 json_utils::find_var_decl(func_name, get_current_func_name(), ast_);
2322 if (
2323 !decl.empty() && decl.contains("value") && decl["value"].is_object() &&
2324 decl["value"].value("_type", "") == "Name" &&
2325 decl["value"].contains("id"))
2326 {
2327 const std::string target =
2328 decl["value"]["id"].template get<std::string>();
2329 if (type_utils::is_builtin_type(target))
2330 {
2331 functions_in_analysis_.erase(func_name);
2332 return target;
2333 }
2334 }
2335 }
2336
2337 functions_in_analysis_.erase(func_name);
2338
2339 std::ostringstream oss;
2340 oss << "Function \"" << func_name << "\" not found (" << python_filename_
2341 << " line " << current_line_ << ")";
2342
2343 throw std::runtime_error(oss.str());
2344}
2345
2346template <class Json>
2347std::string python_annotation<Json>::get_type_from_rhs_variable(
2348 const Json &element,
2349 const Json &body)
2350{
2351 const auto &value_type = element["value"]["_type"];
2352
2353 // Handle subscript of string constant (e.g., "hello"[0] returns str)
2354 if (
2355 value_type == "Subscript" &&
2356 element["value"]["value"]["_type"] == "Constant" &&
2357 element["value"]["value"]["value"].is_string())
2358 {
2359 return "str";
2360 }
2361
2362 // Handle subscript operations (including nested ones)
2363 if (value_type == "Subscript")
2364 return resolve_subscript_type(element["value"], body);
2365
2366 std::string rhs_var_name;
2367
2368 if (value_type == "Name")
2369 rhs_var_name = element["value"]["id"];
2370 else if (value_type == "UnaryOp")
2371 rhs_var_name = element["value"]["operand"]["id"];
2372 else
2373 rhs_var_name = get_base_var_name(element["value"]["value"]);
2374
2375 assert(!rhs_var_name.empty());
2376
2377 auto extract_lhs_name = [](const Json &stmt) -> std::string {
2378 if (
2379 stmt.contains("_type") && stmt["_type"] == "Assign" &&
2380 stmt.contains("targets") && !stmt["targets"].empty() &&
2381 stmt["targets"][0].contains("_type") &&
2382 stmt["targets"][0]["_type"] == "Name" &&
2383 stmt["targets"][0].contains("id"))
2384 return stmt["targets"][0]["id"].template get<std::string>();
2385
2386 if (
2387 stmt.contains("_type") && stmt["_type"] == "AnnAssign" &&
2388 stmt.contains("target") && stmt["target"].contains("_type") &&
2389 stmt["target"]["_type"] == "Name" && stmt["target"].contains("id"))
2390 return stmt["target"]["id"].template get<std::string>();
2391
2392 return "";
2393 };
2394
2395 const std::string lhs_name = extract_lhs_name(element);
2396 if (!lhs_name.empty() && lhs_name == rhs_var_name)
2397 {
2398 std::string lhs_type = get_type_from_lhs(lhs_name, body);
2399 if (!lhs_type.empty())
2400 return lhs_type;
2401 return "Any";
2402 }
2403
2404 if (resolving_rhs_vars_.contains(rhs_var_name))
2405 {
2406 std::string lhs_type = get_type_from_lhs(rhs_var_name, body);
2407 if (!lhs_type.empty())
2408 return lhs_type;
2409 return "Any";
2410 }
2411
2412 resolving_rhs_vars_.insert(rhs_var_name);
2413 struct erase_guardt
2414 {
2415 std::set<std::string> &vars;
2416 std::string key;
2417 ~erase_guardt()
2418 {
2419 vars.erase(key);
2420 }
2421 } erase_guard{resolving_rhs_vars_, rhs_var_name};
2422
2423 // Find RHS variable declaration in the current scope (e.g.: while/if block)
2424 Json rhs_node = find_annotated_assign(rhs_var_name, body["body"]);
2425
2426 // Try to infer variable from current scope
2427 if (rhs_node.empty())
2428 {
2429 auto var = json_utils::get_var_node(rhs_var_name, body);
2430 std::string type;
2431 if (infer_type(var, body, type) == InferResult::OK && !type.empty())
2432 return type;
2433 }
2434
2435 // Find RHS variable declaration in the current function
2436 if (rhs_node.empty() && current_func)
2437 rhs_node = find_annotated_assign(rhs_var_name, (*current_func)["body"]);
2438
2439 // Check current function scope for function parameters
2440 if (
2441 rhs_node.empty() && current_func != nullptr &&
2442 (*current_func).contains("args"))
2443 {
2444 rhs_node =
2445 find_annotated_assign(rhs_var_name, (*current_func)["args"]["args"]);
2446 // Also search keyword-only args
2447 if (rhs_node.empty() && (*current_func)["args"].contains("kwonlyargs"))
2448 rhs_node = find_annotated_assign(
2449 rhs_var_name, (*current_func)["args"]["kwonlyargs"]);
2450 }
2451
2452 // Find RHS variable in the current function args
2453 if (rhs_node.empty() && body.contains("args"))
2454 {
2455 rhs_node = find_annotated_assign(rhs_var_name, body["args"]["args"]);
2456 // Also search keyword-only args
2457 if (rhs_node.empty() && body["args"].contains("kwonlyargs"))
2458 rhs_node =
2459 find_annotated_assign(rhs_var_name, body["args"]["kwonlyargs"]);
2460 }
2461
2462 // Closure capture: fall back to the enclosing function before the
2463 // global scope. Matches the args-then-body order used in
2464 // find_var_node_for_inference.
2465 if (rhs_node.empty() && parent_func != nullptr)
2466 {
2467 if (
2468 (*parent_func).contains("args") &&
2469 (*parent_func)["args"].contains("args"))
2470 {
2471 rhs_node =
2472 find_annotated_assign(rhs_var_name, (*parent_func)["args"]["args"]);
2473 if (rhs_node.empty() && (*parent_func)["args"].contains("kwonlyargs"))
2474 rhs_node = find_annotated_assign(
2475 rhs_var_name, (*parent_func)["args"]["kwonlyargs"]);
2476 }
2477
2478 if (rhs_node.empty())
2479 rhs_node = find_annotated_assign(rhs_var_name, (*parent_func)["body"]);
2480 }
2481
2482 // Find RHS variable node in the global scope
2483 if (rhs_node.empty())
2484 rhs_node = find_annotated_assign(rhs_var_name, ast_["body"]);
2485
2486 if (rhs_node.empty())
2487 {
2488 // Defensive fallback: when the RHS names a Python iterable-producing
2489 // builtin (e.g. `alias = range`), there is no AST declaration to find.
2490 // Return the builtin's mapped type so a bare RHS does not abort type
2491 // inference. Each entry below is safe because `builtin_functions`
2492 // maps it to its own name as the call-result type tag
2493 // ("range" -> "range", ...), which is also a workable placeholder for
2494 // the callable. Do NOT extend this set without verifying the same
2495 // property -- adding e.g. "iter" would return "iterator" as the type
2496 // of the callable itself, which is wrong. ("reversed" is intentionally
2497 // absent: it maps to "list" -- its call-result type, like "sorted" --
2498 // not to a self-named callable placeholder.)
2499 static const std::unordered_set<std::string> iterable_builtins = {
2500 "range", "enumerate", "zip"};
2501 if (iterable_builtins.count(rhs_var_name))
2502 return builtin_functions().at(rhs_var_name);
2503
2504 // A class or a builtin type name bound to a name is a class object; the
2505 // converter gives both the same representation (#7549). The module-level
2506 // pass recognises the builtin names on its own, but a class body and a
2507 // default argument reach inference only through here.
2508 if (
2509 json_utils::is_class(rhs_var_name, ast_) ||
2510 type_utils::is_type_identifier(rhs_var_name) ||
2512 return "type";
2513
2514 // A name read from an import is left to the converter, which types it as
2515 // it does the name used directly (#8366).
2516 if (reads_import(rhs_var_name, element["value"]))
2517 return "";
2518
2519 const auto &lineno = element["lineno"].template get<int>();
2520
2521 std::ostringstream oss;
2522 oss << "Type inference failed at line " << lineno;
2523 oss << ". Variable " << rhs_var_name << " not found";
2524
2525 throw std::runtime_error(oss.str());
2526 }
2527
2528 if (value_type == "Subscript")
2529 {
2530 // Check if annotation exists and is not null before accessing
2531 if (
2532 rhs_node.contains("annotation") && !rhs_node["annotation"].is_null() &&
2533 rhs_node["annotation"].contains("id"))
2534 {
2535 // Get the type of the variable being subscripted
2536 const std::string &var_type = rhs_node["annotation"]["id"];
2537
2538 // Handle string subscript: str[index] returns str
2539 if (var_type == "str")
2540 return "str";
2541 // Handle list subscript: use existing logic
2542 else if (var_type == "list")
2543 {
2544 if (!rhs_node["value"].is_null())
2545 return get_list_subtype(rhs_node["value"]);
2546 else
2547 return "Any"; // Default for unknown list element types
2548 }
2549 // Handle other subscript operations
2550 else
2551 return "Any"; // Default for unknown subscript types
2552 }
2553 else
2554 return "Any"; // Default for unknown subscript types when annotation is
2555 // missing
2556 }
2557
2558 if (
2559 rhs_node.contains("annotation") && !rhs_node["annotation"].is_null() &&
2560 rhs_node["annotation"].contains("id") &&
2561 !rhs_node["annotation"]["id"].is_null())
2562 {
2563 return rhs_node["annotation"]["id"];
2564 }
2565 else
2566 return "Any"; // Default for cases where annotation is missing or null
2567}
2568
2571template <class Json>
2572bool python_annotation<Json>::holds_callable_value(
2573 const std::string &name) const
2574{
2575 const nlohmann::json &body =
2576 static_cast<const nlohmann::json &>(ast_["body"]);
2577 for (const auto &stmt : body)
2578 {
2579 if (!stmt.contains("_type"))
2580 continue;
2581 const std::string &stype = stmt["_type"];
2582 // Top-level assignment: g = f
2583 if (stype == "Assign" && stmt.contains("targets"))
2584 {
2585 for (const auto &t : stmt["targets"])
2586 if (t.contains("id") && t["id"] == name)
2587 return true;
2588 }
2589 // A callable instance: s = S() is already annotated as s: S
2590 if (
2591 stype == "AnnAssign" && stmt["target"].value("id", "") == name &&
2593 body, stmt["annotation"].value("id", ""), "__call__"))
2594 return true;
2595 }
2596 // A local such as a loop variable over callable instances (#8351)
2597 return current_func != nullptr &&
2598 json_utils::count_name_bindings(*current_func, name) > 0;
2599}
2600
2601template <class Json>
2602std::string python_annotation<Json>::get_type_from_call(const Json &element)
2603{
2604 const Json &func = element["value"]["func"];
2605
2606 // Handle regular function calls like func_name()
2607 if (func["_type"] == "Name")
2608 {
2609 const std::string &func_id = func["id"];
2610
2611 const bool is_user_class = json_utils::is_class<Json>(func_id, ast_);
2612 const std::string copied =
2613 is_user_class ? "" : list_copy_type(func_id, element["value"]);
2614 if (!copied.empty())
2615 return copied;
2616
2617 if (
2618 is_user_class || type_utils::is_builtin_type(func_id) ||
2621 return func_id;
2622
2623 if (type_utils::is_consensus_func(func_id))
2625
2626 // Try to resolve overload before falling back to regular function
2628 {
2629 std::string overload_type =
2630 resolve_overload_return_type(func_id, element["value"]);
2631 if (!overload_type.empty())
2632 return overload_type;
2633
2634 // func_id may be a function-pointer variable (not a defined function).
2635 // Catch the "not found" error only when func_id is actually defined in
2636 // this module's AST (as a top-level assignment or function parameter).
2637 // Otherwise re-throw so the caller can report "Function not found".
2638 try
2639 {
2640 return get_function_return_type(func_id, ast_);
2641 }
2642 catch (std::runtime_error &)
2643 {
2644 // Keep imported names intact here; the converter resolves them later.
2645 if (is_imported_name(func_id))
2646 return func_id;
2647 if (
2648 const std::string type =
2649 function_value_return_type(func, &element["value"]);
2650 !type.empty())
2651 return type;
2652 if (holds_callable_value(func_id))
2653 return "";
2654 throw; // func_id is not a known variable: propagate the error
2655 }
2656 }
2657 }
2658
2659 // Handle class method calls like int.from_bytes(), str.join(), etc.
2660 else if (func["_type"] == "Attribute" && func["value"]["_type"] == "Name")
2661 {
2662 const std::string &class_name = func["value"]["id"];
2663 const std::string &method_name = func["attr"];
2664
2665 // Handle built-in type class methods
2666 if (type_utils::is_builtin_type(class_name))
2667 {
2668 // Map specific class methods to their return types
2669 if (class_name == "int" && method_name == "from_bytes")
2670 return "int";
2671 else if (class_name == "int" && method_name == "to_bytes")
2672 return "bytes";
2673 else if (
2674 class_name == "str" &&
2675 (method_name == "join" || method_name == "format"))
2676 return "str";
2677 else if (
2678 class_name == "bytes" &&
2679 (method_name == "decode" || method_name == "hex"))
2680 return "str";
2681 else if (class_name == "bytes" && method_name == "fromhex")
2682 return "bytes";
2683 else if (class_name == "list" && method_name == "copy")
2684 return "list";
2685 else if (
2686 class_name == "dict" &&
2687 (method_name == "copy" || method_name == "fromkeys"))
2688 return "dict";
2689 else
2690 return class_name; // Default: method returns same type as class
2691 }
2692
2693 if (module_manager_)
2694 {
2695 auto module = module_manager_->get_module(class_name);
2696 if (module)
2697 {
2698 auto overloads_funcs = module->overloads();
2699 std::vector<Json> overloads;
2700
2701 for (const auto &elem : overloads_funcs)
2702 {
2703 if (elem["_type"] == "FunctionDef" && elem["name"] == method_name)
2704 overloads.push_back(elem);
2705 }
2706
2707 return match_literal_argument(element["value"], overloads);
2708 }
2709 }
2710 }
2711
2712 return "";
2713}
2714
2717template <class Json>
2718std::optional<std::pair<Json, std::string>>
2719python_annotation<Json>::function_signature(
2721{
2722 const Json &def = *function.def;
2723 const Json &args = def["args"];
2724 if (
2725 !def["decorator_list"].empty() || !args["posonlyargs"].empty() ||
2726 !args["kwonlyargs"].empty() || !args["vararg"].is_null() ||
2727 !args["kwarg"].is_null())
2728 return std::nullopt;
2729 // Each parameter's annotation, null when it has none, and whether it has a
2730 // default, so candidates agree only on both.
2731 Json params = Json::array();
2732 const std::size_t first_default =
2733 args["args"].size() - args["defaults"].size();
2734 for (std::size_t i = 0; i < args["args"].size(); ++i)
2735 params.push_back(
2736 {{"annotation", args["args"][i]["annotation"]},
2737 {"default", i >= first_default}});
2738
2739 const std::string name = def["name"];
2740 const Json &returns = def["returns"];
2741 std::string returned;
2742 if (function.module != &ast_)
2743 returned = returns.is_object() && returns.value("_type", "") == "Name"
2744 ? returns["id"].template get<std::string>()
2745 : "";
2746 else if (json_utils::count_name_bindings(ast_, name) == 1)
2747 {
2748 try
2749 {
2750 returned = get_function_return_type(name, ast_);
2751 }
2752 catch (std::runtime_error &)
2753 {
2754 }
2755 }
2756 if (returned.empty() || returned == "Any")
2757 return std::nullopt;
2758 return std::make_pair(params, returned);
2759}
2760
2764template <class Json>
2765std::string python_annotation<Json>::function_value_return_type(
2766 const Json &value,
2767 const Json *call)
2768{
2769 function_values values(ast_, extra_subscript_inference_asts_);
2770 const auto functions = values.of(value);
2771 if (!functions)
2772 return "";
2773 std::optional<std::pair<Json, std::string>> agreed;
2774 for (const function_values::candidate &function : *functions)
2775 {
2776 // A recursive call through the value returns what the other candidates
2777 // return.
2778 if (
2779 function.module == &ast_ &&
2780 functions_in_analysis_.count((*function.def)["name"]) > 0)
2781 continue;
2782 const auto signature = function_signature(function);
2783 if (
2784 !signature || (agreed && (signature->second != agreed->second ||
2786 signature->first, agreed->first))))
2787 return "";
2788 agreed = signature;
2789 }
2790 if (!agreed)
2791 return "";
2792 const auto is_starred = [](const Json &arg) {
2793 return arg.value("_type", "") == "Starred";
2794 };
2795 const auto required = std::count_if(
2796 agreed->first.begin(), agreed->first.end(), [](const Json &param) {
2797 return !param["default"].template get<bool>();
2798 });
2799 const std::size_t passed = call ? (*call)["args"].size() : 0;
2800 if (
2801 call &&
2802 (passed < static_cast<std::size_t>(required) ||
2803 passed > agreed->first.size() || !(*call)["keywords"].empty() ||
2804 std::any_of((*call)["args"].begin(), (*call)["args"].end(), is_starred)))
2805 return "";
2806 return agreed->second;
2807}
2808
2809template <class Json>
2810std::string python_annotation<Json>::method_return_type(
2811 const Json &member,
2812 const std::string &class_name)
2813{
2814 if (member.contains("returns") && !member["returns"].is_null())
2815 {
2816 const Json &ret = member["returns"];
2817 if (ret.contains("id"))
2818 return ret["id"].template get<std::string>();
2819 if (
2820 ret.contains("_type") && ret["_type"] == "Subscript" &&
2821 ret.contains("value") && ret["value"].contains("id"))
2822 return ret["value"]["id"].template get<std::string>();
2823 }
2824 std::string inferred = infer_method_return_type(member, class_name);
2825 return inferred.empty() ? "Any" : inferred;
2826}
2827
2828template <class Json>
2829std::string python_annotation<Json>::infer_method_return_type(
2830 const Json &member,
2831 const std::string &class_name)
2832{
2833 const std::string method_name = member["name"].template get<std::string>();
2834 const std::string saved_ctx = current_func_name_context_;
2835 current_func_name_context_ = class_name + "@C@" + method_name;
2836 std::string inferred =
2837 infer_from_return_statements(member["body"], method_name);
2838 current_func_name_context_ = saved_ctx;
2839 return inferred;
2840}
2841
2842template <class Json>
2843std::optional<std::string>
2844python_annotation<Json>::get_type_from_literal_method(const Json &call)
2845{
2846 if (!call["func"].contains("value"))
2847 return std::nullopt;
2848 const Json &receiver = call["func"]["value"];
2849 // An f-string has no symbol name either; its methods are str methods.
2850 if (receiver["_type"] == "JoinedStr" && call["func"].contains("attr"))
2851 return get_string_method_return_type(call["func"]["attr"]);
2852 if (receiver["_type"] != "Constant")
2853 return std::nullopt;
2854
2855 std::string obj_type = get_type_from_constant(receiver);
2856
2857 // For string constants, determine return type based on method name
2858 if (obj_type == "str" && call["func"].contains("attr"))
2859 {
2860 const std::string &method = call["func"]["attr"];
2861 return get_string_method_return_type(method);
2862 }
2863
2864 // bytes.hex() returns a str (hex digits), not bytes.
2865 if (
2866 obj_type == "bytes" && call["func"].contains("attr") &&
2867 call["func"]["attr"] == "hex")
2868 return "str";
2869
2870 // (258).to_bytes(...) on an int literal returns bytes.
2871 if (
2872 obj_type == "int" && call["func"].contains("attr") &&
2873 call["func"]["attr"] == "to_bytes")
2874 return "bytes";
2875
2876 // bytes.decode() returns a str.
2877 if (
2878 obj_type == "bytes" && call["func"].contains("attr") &&
2879 call["func"]["attr"] == "decode")
2880 return "str";
2881
2882 return obj_type;
2883}
2884
2885template <class Json>
2886std::string python_annotation<Json>::get_type_from_method(const Json &call)
2887{
2888 std::string type("");
2889
2890 // random.choice / random.sample resolve to the models/random.py variant
2891 // matching the argument's element type, so the base model's `-> int` is not
2892 // this call's return type. Defer to the argument, as min/max/sum do by being
2893 // "Any" in builtin_functions() (issue #7673).
2894 if (
2895 call["func"].contains("value") && call["func"]["value"].is_object() &&
2896 call["func"]["value"].value("_type", std::string()) == "Name" &&
2897 call["func"]["value"].value("id", std::string()) == "random" &&
2898 call["func"].contains("attr"))
2899 {
2900 const std::string &method = call["func"]["attr"];
2901 if (method == "choice")
2902 return "Any";
2903 if (method == "sample")
2904 return "list";
2905 }
2906
2907 // Handle method calls on literals: for " ".join(l) or f"{x}".upper() the
2908 // receiver has no symbol name, so looking it up in the AST would fail.
2909 if (const auto literal_type = get_type_from_literal_method(call))
2910 return *literal_type;
2911
2912 // An f-string receiver is always a str: f"{x}".upper().
2913 if (
2914 call["func"].contains("value") &&
2915 call["func"]["value"]["_type"] == "JoinedStr" &&
2916 call["func"].contains("attr"))
2917 return get_string_method_return_type(call["func"]["attr"]);
2918
2919 // Handle method calls on binary expressions like (s + ",end").split(",", 1)
2920 if (
2921 call["func"].contains("value") && call["func"]["value"]["_type"] == "BinOp")
2922 {
2923 std::string obj_type =
2924 get_type_from_binary_expr(call["func"]["value"], ast_);
2925 if (obj_type == "str" && call["func"].contains("attr"))
2926 {
2927 const std::string &method = call["func"]["attr"];
2928 return get_string_method_return_type(method);
2929 }
2930 if (!obj_type.empty())
2931 return obj_type;
2932 }
2933
2934 // Inline dict-literal method calls, e.g. `{"a":1}.get("x", 3)`.
2935 // The receiver has no symbol name, so resolve the types we model here
2936 // (get/setdefault/pop/keys/values/items)
2937 // and let anything else fall through to the regular resolution path below.
2938 if (call["func"].contains("value"))
2939 {
2940 const std::string &recv_kind =
2941 call["func"]["value"].value("_type", std::string());
2942 if (recv_kind == "Dict")
2943 {
2944 const std::string method =
2945 call["func"].contains("attr")
2946 ? call["func"]["attr"].template get<std::string>()
2947 : std::string();
2948
2949 // get/setdefault/pop: return type comes from the default arg.
2950 if (method == "setdefault" || method == "get" || method == "pop")
2951 {
2952 std::string t = infer_type_from_default_arg_shape(call["args"]);
2953 if (!t.empty())
2954 return t;
2955 }
2956
2957 // keys/values/items: views behave like lists for type inference.
2958 if (method == "keys" || method == "values" || method == "items")
2959 return "list";
2960 }
2961 }
2962
2963 // Handle method calls on subscript expressions: e.g. nested[i].pop()
2964 // get_object_name() returns "" for Subscript nodes (no "id" field), which
2965 // would cause a spurious "Object not found" throw further below.
2966 if (
2967 call["func"].contains("value") &&
2968 call["func"]["value"]["_type"] == "Subscript")
2969 {
2970 std::string method = call["func"].contains("attr")
2971 ? call["func"]["attr"].template get<std::string>()
2972 : "";
2973 if (method == "pop")
2974 {
2975 // Try to infer the element type of the inner list from the base list.
2976 const auto &subscript = call["func"]["value"];
2977 if (
2978 subscript.contains("value") && subscript["value"]["_type"] == "Name" &&
2979 subscript["value"].contains("id"))
2980 {
2981 const std::string &base_name =
2982 subscript["value"]["id"].template get<std::string>();
2983 Json base_decl =
2984 json_utils::find_var_decl(base_name, get_current_func_name(), ast_);
2985 if (!base_decl.empty() && base_decl.contains("value"))
2986 {
2987 const Json &rhs = base_decl["value"];
2988 // List comprehension [[...] for _ in range(n)]: elt is the inner list
2989 if (rhs["_type"] == "ListComp" && rhs.contains("elt"))
2990 {
2991 std::string inner_type = get_argument_type(rhs["elt"]);
2992 // Strip "list[T]" → "T" (inner list element type is T)
2993 if (
2994 inner_type.size() > 5 && inner_type.substr(0, 5) == "list[" &&
2995 inner_type.back() == ']')
2996 {
2997 return inner_type.substr(5, inner_type.size() - 6);
2998 }
2999 }
3000 }
3001 }
3002 }
3003 // Cannot infer type — signal unknown to caller
3004 return "";
3005 }
3006
3007 const std::string &obj = get_object_name(call["func"], std::string());
3008 std::string attr_name = call["func"].contains("attr")
3009 ? call["func"]["attr"].template get<std::string>()
3010 : "";
3011
3012 // Handle super().method() calls: look up the method in the parent class.
3013 // Only handles simple base class names; dotted names (e.g. module.Base)
3014 // and multiple-inheritance MRO traversal beyond the first base are not
3015 // supported — they fall back to "Any".
3016 if (obj == "super" && !attr_name.empty() && !current_class_name_.empty())
3017 {
3018 Json class_node = json_utils::find_class(ast_["body"], current_class_name_);
3019 if (
3020 !class_node.empty() && class_node.contains("bases") &&
3021 !class_node["bases"].empty() && class_node["bases"][0].contains("id"))
3022 {
3023 const std::string base_name =
3024 class_node["bases"][0]["id"].template get<std::string>();
3025 Json base_class = json_utils::find_class(ast_["body"], base_name);
3026 if (!base_class.empty())
3027 {
3028 for (const Json &member : base_class["body"])
3029 {
3030 if (member["_type"] != "FunctionDef" || member["name"] != attr_name)
3031 continue;
3032 return method_return_type(member, base_name);
3033 }
3034 }
3035 }
3036 return "Any";
3037 }
3038
3039 // Handle self.method() calls: resolve the method in the current class (or,
3040 // failing that, along its base chain) and adopt its return type. Without
3041 // this the `self` receiver is unresolved, so an unannotated
3042 // `self.attr = self.method()` is left untyped and the attribute defaults to
3043 // its enclosing class — which later emits a call to a nonexistent method on
3044 // that attribute and aborts GOTO conversion (#6242).
3045 if (obj == "self" && !attr_name.empty() && !current_class_name_.empty())
3046 {
3047 // `visited` guards against cyclic base declarations (e.g. class A(B) /
3048 // class B(A)): these do not run in Python but ast.parse still accepts
3049 // them, so the walk must not loop forever.
3050 std::set<std::string> visited;
3051 for (std::string cls = current_class_name_;
3052 !cls.empty() && visited.insert(cls).second;)
3053 {
3054 Json class_node = json_utils::find_class(ast_["body"], cls);
3055 if (class_node.empty() || !class_node.contains("body"))
3056 break;
3057 for (const Json &member : class_node["body"])
3058 {
3059 if (member["_type"] != "FunctionDef" || member["name"] != attr_name)
3060 continue;
3061 return method_return_type(member, cls);
3062 }
3063 // Not defined in this class — continue up the first base, as super()
3064 // does.
3065 cls.clear();
3066 if (
3067 class_node.contains("bases") && !class_node["bases"].empty() &&
3068 class_node["bases"][0].contains("id"))
3069 cls = class_node["bases"][0]["id"].template get<std::string>();
3070 }
3071 }
3072
3073 // Handle dict.keys() and dict.values()
3074 if (attr_name == "keys" || attr_name == "values")
3075 {
3076 Json obj_node =
3077 json_utils::find_var_decl(obj, get_current_func_name(), ast_);
3078
3079 // Check function parameters if not found in body
3080 if (
3081 obj_node.empty() && current_func != nullptr &&
3082 (*current_func).contains("args") &&
3083 (*current_func)["args"].contains("args"))
3084 {
3085 obj_node = find_annotated_assign(obj, (*current_func)["args"]["args"]);
3086 }
3087
3088 if (
3089 !obj_node.empty() && obj_node.contains("annotation") &&
3090 !obj_node["annotation"].is_null())
3091 {
3092 const Json &annotation = obj_node["annotation"];
3093 std::string base_type;
3094
3095 // Handle simple dict annotation
3096 if (annotation.contains("id"))
3097 base_type = annotation["id"].template get<std::string>();
3098 // Handle generic dict[K,V] annotation (Subscript node)
3099 else if (
3100 annotation.contains("_type") && annotation["_type"] == "Subscript" &&
3101 annotation.contains("value") && annotation["value"].contains("id"))
3102 base_type = annotation["value"]["id"].template get<std::string>();
3103
3104 // If it's a dict type, extract key/value types
3105 if (base_type == "dict")
3106 {
3107 // For dict[K,V], extract the K and V types
3108 if (
3109 annotation.contains("_type") && annotation["_type"] == "Subscript" &&
3110 annotation.contains("slice"))
3111 {
3112 const Json &slice = annotation["slice"];
3113
3114 // dict[K,V] has slice as Tuple with 2 elements
3115 if (
3116 slice.contains("_type") && slice["_type"] == "Tuple" &&
3117 slice.contains("elts") && slice["elts"].size() >= 2)
3118 {
3119 std::string key_type, value_type;
3120
3121 // Extract key type (first element)
3122 if (slice["elts"][0].contains("id"))
3123 key_type = slice["elts"][0]["id"].template get<std::string>();
3124
3125 // Extract value type (second element)
3126 if (slice["elts"][1].contains("id"))
3127 value_type = slice["elts"][1]["id"].template get<std::string>();
3128
3129 // Return appropriate list type based on method
3130 if (attr_name == "keys" && !key_type.empty())
3131 return "list[" + key_type + "]";
3132 else if (attr_name == "values" && !value_type.empty())
3133 return "list[" + value_type + "]";
3134 }
3135 }
3136 }
3137 }
3138 }
3139
3140 // Get type from imported module
3141 if (module_manager_)
3142 {
3143 // Try to get module using the object name directly
3144 auto module = module_manager_->get_module(obj);
3145
3146 // If not found, try using get_object_alias to resolve import aliases
3147 if (!module && !obj.empty())
3148 {
3149 std::string resolved_obj = json_utils::get_object_alias(ast_, obj);
3150 if (resolved_obj != obj)
3151 {
3152 module = module_manager_->get_module(resolved_obj);
3153 }
3154 }
3155
3156 if (module)
3157 {
3158 // First try as function
3159 function func = module->get_function(attr_name);
3160 if (!func.name_.empty())
3161 {
3162 return func.return_type_;
3163 }
3164
3165 // If not a function, try as class
3166 class_definition cls = module->get_class(attr_name);
3167 if (!cls.name_.empty())
3168 {
3169 // Return the class name as the type for constructor calls
3170 return cls.name_;
3171 }
3172
3173 // If module exists but attribute not found, don't continue to search
3174 // in AST (which would fail for imported modules)
3175 return "";
3176 }
3177 }
3178
3179 Json obj_node = find_receiver_decl(obj);
3180
3181 // Check current function parameters if not found
3182 if (
3183 obj_node.empty() && current_func != nullptr &&
3184 (*current_func).contains("args") &&
3185 (*current_func)["args"].contains("args"))
3186 {
3187 obj_node = find_annotated_assign(obj, (*current_func)["args"]["args"]);
3188 }
3189
3190 // Handle nested attribute access (e.g., self.f.foo() or self.b.a.get_value())
3191 if (obj_node.empty() && call["func"]["value"]["_type"] == "Attribute")
3192 {
3193 // Recursively resolve nested attribute chain (e.g., self.b.a -> [self, b,
3194 // a])
3195 std::function<std::string(const Json &, std::vector<std::string> &)>
3196 extract_attr_chain =
3197 [&](const Json &node, std::vector<std::string> &chain) -> std::string {
3198 if (node["_type"] == "Attribute")
3199 {
3200 std::string attr = node["attr"].template get<std::string>();
3201 chain.push_back(attr);
3202 return extract_attr_chain(node["value"], chain);
3203 }
3204 else if (node["_type"] == "Name" && node.contains("id"))
3205 {
3206 return node["id"].template get<std::string>();
3207 }
3208 return "";
3209 };
3210
3211 std::vector<std::string> attr_chain;
3212 std::string base_obj =
3213 extract_attr_chain(call["func"]["value"], attr_chain);
3214
3215 // Reverse the chain because extract_attr_chain collects from outer to inner
3216 // For self.b.a, we get [a, b] but need [b, a] to process left to right
3217 std::reverse(attr_chain.begin(), attr_chain.end());
3218
3219 // Determine the initial class type of the base object.
3220 // Handles both "self" (look up current class) and arbitrary variables
3221 // (look up variable type from annotation or constructor call).
3222 std::string initial_type;
3223 if (base_obj == "self" && current_func != nullptr)
3224 {
3225 for (const Json &elem : ast_["body"])
3226 {
3227 if (elem["_type"] == "ClassDef" && elem.contains("body"))
3228 {
3229 for (const Json &member : elem["body"])
3230 {
3231 if (
3232 member["_type"] == "FunctionDef" &&
3233 member["name"] == (*current_func)["name"])
3234 {
3235 initial_type = elem["name"].template get<std::string>();
3236 break;
3237 }
3238 }
3239 if (!initial_type.empty())
3240 break;
3241 }
3242 }
3243 }
3244 else if (!base_obj.empty())
3245 {
3246 Json var_node =
3247 json_utils::find_var_decl(base_obj, get_current_func_name(), ast_);
3248 if (!var_node.empty())
3249 {
3250 // AnnAssign: a: A = ...
3251 if (
3252 var_node.contains("annotation") &&
3253 !var_node["annotation"].is_null() &&
3254 var_node["annotation"].contains("id"))
3255 initial_type =
3256 var_node["annotation"]["id"].template get<std::string>();
3257 // Assign: a = A()
3258 else if (
3259 var_node.contains("value") && var_node["value"].contains("_type") &&
3260 var_node["value"]["_type"] == "Call" &&
3261 var_node["value"].contains("func") &&
3262 var_node["value"]["func"].contains("_type") &&
3263 var_node["value"]["func"]["_type"] == "Name" &&
3264 var_node["value"]["func"].contains("id"))
3265 initial_type =
3266 var_node["value"]["func"]["id"].template get<std::string>();
3267 }
3268 // Also check function parameters
3269 if (
3270 initial_type.empty() && current_func != nullptr &&
3271 (*current_func).contains("args") &&
3272 (*current_func)["args"].contains("args"))
3273 {
3274 Json param =
3275 find_annotated_assign(base_obj, (*current_func)["args"]["args"]);
3276 if (
3277 !param.empty() && param.contains("annotation") &&
3278 !param["annotation"].is_null() && param["annotation"].contains("id"))
3279 initial_type = param["annotation"]["id"].template get<std::string>();
3280 }
3281 }
3282
3283 if (!initial_type.empty() && !attr_chain.empty())
3284 {
3285 std::string current_type = initial_type;
3286 for (size_t i = 0; i < attr_chain.size(); ++i)
3287 {
3288 const std::string &chain_attr = attr_chain[i];
3289 Json current_class = json_utils::find_class(ast_["body"], current_type);
3290 if (current_class.empty())
3291 break;
3292
3293 bool found = false;
3294 for (const Json &member : current_class["body"])
3295 {
3296 if (member["_type"] != "FunctionDef" || member["name"] != "__init__")
3297 continue;
3298 for (const Json &stmt : member["body"])
3299 {
3300 // AnnAssign: self.attr: Type = value
3301 if (
3302 stmt.contains("_type") && stmt["_type"] == "AnnAssign" &&
3303 stmt.contains("target") && stmt["target"].is_object() &&
3304 stmt["target"].contains("_type") &&
3305 stmt["target"]["_type"] == "Attribute" &&
3306 stmt["target"].contains("value") &&
3307 stmt["target"]["value"].is_object() &&
3308 stmt["target"]["value"].contains("id") &&
3309 stmt["target"]["value"]["id"] == "self" &&
3310 stmt["target"]["attr"] == chain_attr &&
3311 stmt.contains("annotation") && !stmt["annotation"].is_null() &&
3312 stmt["annotation"].contains("id") &&
3313 !stmt["annotation"]["id"].is_null())
3314 {
3315 current_type =
3316 stmt["annotation"]["id"].template get<std::string>();
3317 found = true;
3318 break;
3319 }
3320 // Assign: self.attr = SomeClass()
3321 if (
3322 stmt.contains("_type") && stmt["_type"] == "Assign" &&
3323 stmt.contains("targets") && stmt["targets"].is_array() &&
3324 !stmt["targets"].empty() && stmt["targets"][0].is_object() &&
3325 stmt["targets"][0].contains("_type") &&
3326 stmt["targets"][0]["_type"] == "Attribute" &&
3327 stmt["targets"][0].contains("value") &&
3328 stmt["targets"][0]["value"].is_object() &&
3329 stmt["targets"][0]["value"].contains("id") &&
3330 stmt["targets"][0]["value"]["id"] == "self" &&
3331 stmt["targets"][0]["attr"] == chain_attr &&
3332 stmt.contains("value") && stmt["value"].contains("_type") &&
3333 stmt["value"]["_type"] == "Call" &&
3334 stmt["value"].contains("func") &&
3335 stmt["value"]["func"].contains("_type") &&
3336 stmt["value"]["func"]["_type"] == "Name" &&
3337 stmt["value"]["func"].contains("id"))
3338 {
3339 current_type =
3340 stmt["value"]["func"]["id"].template get<std::string>();
3341 found = true;
3342 break;
3343 }
3344 }
3345 if (found)
3346 break;
3347 }
3348
3349 if (!found)
3350 break;
3351
3352 if (i == attr_chain.size() - 1)
3353 {
3354 Json final_class = json_utils::find_class(ast_["body"], current_type);
3355 if (!final_class.empty())
3356 {
3357 const std::string method_name =
3358 call["func"]["attr"].template get<std::string>();
3359 for (const Json &method : final_class["body"])
3360 {
3361 if (
3362 method["_type"] == "FunctionDef" &&
3363 method["name"] == method_name)
3364 return method_return_type(method, current_type);
3365 }
3366 }
3367 // Chain resolved but method not in final class
3368 return "";
3369 }
3370 }
3371 }
3372 // Could not resolve chain - return empty rather than throwing
3373 return "";
3374 }
3375
3376 if (obj_node.empty())
3377 {
3378 // Method call on temporary expression (e.g., ({1,2,3}-{1}).pop()).
3379 // Such values have no symbol name, so object lookup fails.
3380 if (
3381 obj.empty() && call["func"].contains("value") &&
3382 call["func"]["value"].contains("_type") &&
3383 call["func"]["value"]["_type"] == "BinOp")
3384 {
3385 std::string inferred_expr_type =
3386 get_type_from_binary_expr(call["func"]["value"], ast_);
3387 return inferred_expr_type.empty() ? "Any" : inferred_expr_type;
3388 }
3389
3390 // Check if obj is a class name
3391 Json class_node = json_utils::find_class(ast_["body"], obj);
3392 if (!class_node.empty())
3393 {
3394 const std::string &method_name = call["func"]["attr"];
3395 // Find the method in the class body
3396 for (const Json &member : class_node["body"])
3397 {
3398 if (member["_type"] == "FunctionDef" && member["name"] == method_name)
3399 {
3400 // Prefer the explicit return-type annotation when present.
3401 if (member.contains("returns") && !member["returns"].is_null())
3402 {
3403 const Json &ret = member["returns"];
3404 // Simple type: -> str, -> int, -> MyClass
3405 if (ret.contains("id"))
3406 return ret["id"].template get<std::string>();
3407 // Generic type: -> dict[str, bool], -> list[int], etc.
3408 if (
3409 ret.contains("_type") && ret["_type"] == "Subscript" &&
3410 ret.contains("value") && ret["value"].contains("id"))
3411 return ret["value"]["id"].template get<std::string>();
3412 }
3413 std::string inferred_type = infer_method_return_type(member, obj);
3414 if (!inferred_type.empty())
3415 return inferred_type;
3416 }
3417 }
3418 }
3419
3420 // Method call chained on another call's result, e.g.
3421 // ",".join([...]).split(",") or "a-b".replace("-", ".").split("."). The
3422 // inner receiver is a temporary with no symbol name and no class to
3423 // resolve, so report an unknown type instead of throwing.
3424 if (
3425 call["func"].contains("value") &&
3426 call["func"]["value"].contains("_type") &&
3427 call["func"]["value"]["_type"] == "Call")
3428 return "Any";
3429
3430 // A from-imported receiver has no declaration in this module; leave the
3431 // type unknown so the converter resolves the call (#8375).
3432 if (is_imported_name(obj))
3433 return "";
3434
3435 throw std::runtime_error("Object \"" + obj + "\" not found.");
3436 }
3437
3438 std::string obj_type;
3439 if (
3440 obj_node.contains("annotation") && !obj_node["annotation"].is_null() &&
3441 obj_node["annotation"].contains("id") &&
3442 !obj_node["annotation"]["id"].is_null())
3443 {
3444 obj_type = obj_node["annotation"]["id"].template get<std::string>();
3445 }
3446 else
3447 obj_type = "Any"; // Default fallback type
3448
3449 // Handle built-in types
3450 if (type_utils::is_builtin_type(obj_type))
3451 {
3452 if (
3453 obj_type == "str" && call["func"].contains("attr") &&
3454 (call["func"]["attr"] == "split" || call["func"]["attr"] == "rsplit"))
3455 return "list";
3456 // bytes.hex() returns a str (hex digits), not bytes.
3457 if (
3458 obj_type == "bytes" && call["func"].contains("attr") &&
3459 call["func"]["attr"] == "hex")
3460 return "str";
3461 // int.to_bytes() returns bytes. This is the instance form x.to_bytes(...)
3462 // on an int variable; the int.to_bytes(x, ...) class form is mapped in
3463 // get_type_from_call. Without this the receiver's "int" type propagates to
3464 // the assignment target, so the bytes result is mistyped as a scalar int
3465 // and subscript/len on it fail.
3466 if (
3467 obj_type == "int" && call["func"].contains("attr") &&
3468 call["func"]["attr"] == "to_bytes")
3469 return "bytes";
3470 // str.encode() returns bytes; bytes.decode() returns str.
3471 if (
3472 obj_type == "str" && call["func"].contains("attr") &&
3473 call["func"]["attr"] == "encode")
3474 return "bytes";
3475 if (
3476 obj_type == "bytes" && call["func"].contains("attr") &&
3477 call["func"]["attr"] == "decode")
3478 return "str";
3479 // These return an index or a count, never the receiver's type (#9054).
3480 if (
3481 (obj_type == "str" || obj_type == "list" || obj_type == "bytes") &&
3482 call["func"].contains("attr") &&
3483 (call["func"]["attr"] == "index" || call["func"]["attr"] == "rindex" ||
3484 call["func"]["attr"] == "count" || call["func"]["attr"] == "find" ||
3485 call["func"]["attr"] == "rfind"))
3486 return "int";
3487 // list.pop() returns an element, never the list (#4797).
3488 if (
3489 obj_type == "list" && call["func"].contains("attr") &&
3490 call["func"]["attr"] == "pop")
3491 return "Any";
3492 // setdefault/get/pop return the value, not the dict — recover its
3493 // container shape from the default arg when the dict is untyped.
3494 // When that fails (no default arg supplied, e.g. ``d.get(key)``),
3495 // fall back to "Any" rather than the receiver's "dict" type: the
3496 // returned value is an element, never the container itself, and
3497 // letting "dict" propagate up causes the assignment-LHS symbol to
3498 // be created as __python_dict__ — the dict-handler then emits a
3499 // d.keys/d.values access whose member type later trips a member2t
3500 // assertion at symex time (tracked in #4682).
3501 if (
3502 obj_type == "dict" && call["func"].contains("attr") &&
3503 (call["func"]["attr"] == "setdefault" || call["func"]["attr"] == "get" ||
3504 call["func"]["attr"] == "pop"))
3505 {
3506 std::string t = infer_type_from_default_arg_shape(call["args"]);
3507 if (!t.empty())
3508 return t;
3509 return "Any";
3510 }
3511 type = obj_type;
3512 }
3513 else
3514 {
3515 // Handle user-defined class methods
3516 Json class_node = json_utils::find_class(ast_["body"], obj_type);
3517 if (!class_node.empty())
3518 {
3519 const std::string &method_name = call["func"]["attr"];
3520
3521 // Find the method in the class body
3522 for (const Json &member : class_node["body"])
3523 {
3524 if (member["_type"] == "FunctionDef" && member["name"] == method_name)
3525 {
3526 // Get return type from annotation
3527 if (
3528 member.contains("returns") && !member["returns"].is_null() &&
3529 member["returns"].contains("id"))
3530 {
3531 return member["returns"]["id"];
3532 }
3533 }
3534 }
3535 }
3536 }
3537
3538 return type;
3539}
3540
3541// Method to infer type from conditional expressions (IfExp)
3542template <class Json>
3543InferResult python_annotation<Json>::infer_type_from_ifexp(
3544 const Json &ifexp_node,
3545 const Json &body,
3546 std::string &inferred_type)
3547{
3548 // For conditional expressions like "x + y if x > y else x * y"
3549 // We need to infer the type from both the body and orelse branches
3550
3551 // Create temporary statement nodes for type inference
3552 Json body_stmt = {
3553 {"_type", "Assign"},
3554 {"value", ifexp_node["body"]},
3555 {"lineno", current_line_}};
3556
3557 Json orelse_stmt = {
3558 {"_type", "Assign"},
3559 {"value", ifexp_node["orelse"]},
3560 {"lineno", current_line_}};
3561
3562 std::string body_type, orelse_type;
3563
3564 // Infer type from the body (true branch)
3565 InferResult body_result = infer_type(body_stmt, body, body_type);
3566
3567 // Infer type from the orelse (false branch)
3568 InferResult orelse_result = infer_type(orelse_stmt, body, orelse_type);
3569
3570 // If both branches have the same type, return that type
3571 if (body_result == InferResult::OK && orelse_result == InferResult::OK)
3572 {
3573 if (body_type == orelse_type)
3574 {
3575 inferred_type = body_type;
3576 return InferResult::OK;
3577 }
3578
3579 // Handle numeric type promotion: if one is int and other is float, result
3580 // is float
3581 if (
3582 (body_type == "int" && orelse_type == "float") ||
3583 (body_type == "float" && orelse_type == "int"))
3584 {
3585 inferred_type = "float";
3586 return InferResult::OK;
3587 }
3588
3589 // For different types, use a safe fallback
3591 "Conditional expression has different types in branches: {} vs {}. "
3592 "Using 'Any' as fallback ({}:{})",
3593 body_type,
3594 orelse_type,
3595 python_filename_,
3596 current_line_);
3597 inferred_type = "Any";
3598 return InferResult::OK;
3599 }
3600
3601 // If only one branch succeeded, use that type
3602 if (body_result == InferResult::OK)
3603 {
3604 inferred_type = body_type;
3605 return InferResult::OK;
3606 }
3607 if (orelse_result == InferResult::OK)
3608 {
3609 inferred_type = orelse_type;
3610 return InferResult::OK;
3611 }
3612
3613 // Neither arm known (e.g. attributes of unannotated parameters): leave the
3614 // assignment to the converter, as for a bare attribute.
3615 return InferResult::UNKNOWN;
3616}
3617
3618// A numpy.random call that returns an array is annotated as the list a
3619// numpy constructor is, so arithmetic on it is inferred as an array too.
3620template <class Json>
3621std::string
3622python_annotation<Json>::get_type_from_call_or_random(const Json &element)
3623{
3624 const std::string random = numpy_random::list_annotation(
3625 static_cast<const nlohmann::json &>(element["value"]),
3626 static_cast<const nlohmann::json &>(ast_));
3627 return random.empty() ? get_type_from_call(element) : random;
3628}
3629
3630template <class Json>
3631bool python_annotation<Json>::is_numpy_array_ctor_call(
3632 const Json &call_value) const
3633{
3634 if (
3635 !call_value.is_object() ||
3636 call_value.value("_type", std::string()) != "Call" ||
3637 !call_value.contains("func") || !call_value["func"].is_object() ||
3638 call_value["func"].value("_type", std::string()) != "Attribute" ||
3639 !call_value["func"].contains("value") ||
3640 !call_value["func"]["value"].is_object() ||
3641 call_value["func"]["value"].value("_type", std::string()) != "Name")
3642 return false;
3643
3644 static const std::set<std::string> numpy_array_ctors = {
3645 "array",
3646 "zeros",
3647 "ones",
3648 "full",
3649 "empty",
3650 "arange",
3651 "eye",
3652 "identity",
3653 "linspace"};
3654 const std::string method_name = call_value["func"].value("attr", "");
3655 const std::string module_alias = call_value["func"]["value"].value("id", "");
3656 return numpy_array_ctors.count(method_name) != 0 &&
3658 static_cast<const nlohmann::json &>(ast_), module_alias);
3659}
3660
3661template <class Json>
3662bool python_annotation<Json>::current_func_returns_name_directly(
3663 const std::string &name) const
3664{
3665 if (current_func == nullptr || !current_func->contains("body"))
3666 return false;
3667
3668 const Json *block = &(*current_func)["body"];
3669 while (block->is_array() && !block->empty())
3670 {
3671 const Json &last = block->back();
3672 if (!last.is_object())
3673 return false;
3674
3675 const std::string type = last.value("_type", std::string());
3676 if (type == "Return")
3677 return last.contains("value") && last["value"].is_object() &&
3678 last["value"].value("_type", std::string()) == "Name" &&
3679 last["value"].value("id", std::string()) == name;
3680
3681 if (
3682 type != "If" || !last.contains("body") || !last["body"].is_array() ||
3683 !last.contains("orelse") || !last["orelse"].is_array() ||
3684 last["orelse"].empty())
3685 return false;
3686
3687 // Only the trailing if/else's own two arms matter here -- reuse this
3688 // loop for the "then" arm and recurse once for the "else" arm, mirroring
3689 // the two-branch pattern get_function_definition's own check accepts.
3690 bool else_returns = false;
3691 {
3692 const Json *else_block = &last["orelse"];
3693 if (!else_block->is_array() || else_block->empty())
3694 return false;
3695 const Json &else_last = else_block->back();
3696 else_returns =
3697 else_last.is_object() &&
3698 else_last.value("_type", std::string()) == "Return" &&
3699 else_last.contains("value") && else_last["value"].is_object() &&
3700 else_last["value"].value("_type", std::string()) == "Name" &&
3701 else_last["value"].value("id", std::string()) == name;
3702 }
3703 if (!else_returns)
3704 return false;
3705 block = &last["body"];
3706 }
3707 return false;
3708}
3709
3710template <class Json>
3711InferResult python_annotation<Json>::infer_type(
3712 const Json &stmt,
3713 const Json &body,
3714 std::string &inferred_type)
3715{
3716 if (stmt.empty())
3717 return InferResult::UNKNOWN;
3718
3719 if (stmt["_type"] == "arg")
3720 {
3721 if (!stmt.contains("annotation") || stmt["annotation"].is_null())
3722 return InferResult::UNKNOWN;
3723
3724 // A forward-reference (string) annotation `node: 'Foo'` is a Constant whose
3725 // value is the type-name string; a dotted annotation `node: mod.Robot` /
3726 // `node: a.b.Robot` is an Attribute whose last component (attr) is the type
3727 // name. Reading annotation["value"]["id"] blindly does operator[] on a JSON
3728 // string / a null and aborts with nlohmann type_error (#6284), so dispatch
3729 // on the node shape instead. The Attribute branch must precede the
3730 // value.id branch: for a single-dot annotation the Attribute's value is a
3731 // Name holding the module prefix (`mod`), so value.id would otherwise pick
3732 // the prefix rather than the type name (`Robot`).
3733 if (
3734 stmt["annotation"].contains("_type") &&
3735 stmt["annotation"]["_type"] == "Constant" &&
3736 stmt["annotation"].contains("value") &&
3737 stmt["annotation"]["value"].is_string())
3738 inferred_type = stmt["annotation"]["value"].template get<std::string>();
3739 else if (
3740 stmt["annotation"].contains("_type") &&
3741 stmt["annotation"]["_type"] == "Attribute" &&
3742 stmt["annotation"].contains("attr"))
3743 inferred_type = stmt["annotation"]["attr"].template get<std::string>();
3744 else if (
3745 stmt["annotation"].contains("value") &&
3746 stmt["annotation"]["value"].is_object() &&
3747 stmt["annotation"]["value"].contains("id"))
3748 inferred_type =
3749 stmt["annotation"]["value"]["id"].template get<std::string>();
3750 else if (stmt["annotation"].contains("id"))
3751 inferred_type = stmt["annotation"]["id"].template get<std::string>();
3752 else if (
3753 stmt["annotation"].contains("_type") &&
3754 stmt["annotation"]["_type"] == "BinOp")
3755 {
3756 // Handle union types such as str | None (PEP 604 syntax)
3757 const auto &left = stmt["annotation"]["left"];
3758 const auto &right = stmt["annotation"]["right"];
3759
3760 // Check which side is None and extract the other type
3761 bool left_is_none =
3762 (left.contains("_type") && left["_type"] == "Constant" &&
3763 left.contains("value") && left["value"].is_null());
3764 bool right_is_none =
3765 (right.contains("_type") && right["_type"] == "Constant" &&
3766 right.contains("value") && right["value"].is_null());
3767
3768 if (right_is_none && left.contains("id"))
3769 inferred_type = left["id"].template get<std::string>();
3770 else if (left_is_none && right.contains("id"))
3771 inferred_type = right["id"].template get<std::string>();
3772 else
3773 return InferResult::UNKNOWN;
3774 }
3775 else
3776 return InferResult::UNKNOWN;
3777
3778 return InferResult::OK;
3779 }
3780
3781 // Guard against bare return (value=null) or missing value key
3782 if (!stmt.contains("value") || stmt["value"].is_null())
3783 return InferResult::UNKNOWN;
3784
3785 const auto &value_type = stmt["value"]["_type"];
3786
3787 // Get type from RHS constant
3788 if (value_type == "Constant")
3789 inferred_type = get_type_from_constant(stmt["value"]);
3790 else if (value_type == "List")
3791 {
3792 // Non-empty literals stay as a bare ``list`` so the converter keeps its
3793 // permissive, per-element (possibly heterogeneous) handling.
3794 inferred_type = "list";
3795
3796 // An *empty* literal leaves the element type unresolved, so a loop
3797 // variable bound from it (e.g. the ``for p in primes`` synthesised
3798 // from ``all(... for p in primes)``) gets an inconsistent bit-width
3799 // and trips assert_arith_2ops_consistency in integer arithmetic
3800 // (issue #3239). Recover the element type from the first
3801 // ``<target>.append(arg)`` in the same scope, but only commit to a
3802 // concrete ``list[int]`` when that argument actually resolves to an
3803 // int -- otherwise (object/float/str/unresolved) keep the permissive
3804 // bare ``list`` so object lists such as ``result = []; result.append(obj)``
3805 // are unaffected.
3806 const auto &elts = stmt["value"];
3807 if (
3808 (!elts.contains("elts") || elts["elts"].empty()) &&
3809 stmt.contains("targets") && stmt["targets"].is_array() &&
3810 !stmt["targets"].empty() && stmt["targets"][0].is_object() &&
3811 stmt["targets"][0].value("_type", std::string()) == "Name")
3812 {
3813 const std::string target_name =
3814 stmt["targets"][0].value("id", std::string());
3815 if (!target_name.empty())
3816 {
3817 // Only commit to list[int] when the first append argument resolves
3818 // to a *direct* integer scalar. An argument that borrows its
3819 // int-ness through an indirection -- e.g. ``result.append(actions[i])``
3820 // where ``actions`` is a heterogeneous object list mis-typed as
3821 // list[int] -- must keep the permissive bare ``list``, otherwise the
3822 // object elements collapse to ints and later attribute/method
3823 // resolution fails (jpl_1 regression).
3824 Json append_arg = find_first_append_arg(body, target_name);
3825 if (!append_arg.empty() && append_arg_is_plain_int(append_arg))
3826 inferred_type = "list[int]";
3827 }
3828 }
3829 }
3830 else if (value_type == "Set")
3831 inferred_type = "set";
3832 else if (value_type == "Tuple")
3833 inferred_type = "tuple";
3834 else if (value_type == "Dict")
3835 inferred_type = get_dict_literal_type(stmt["value"]);
3836 else if (value_type == "Compare")
3837 inferred_type = "bool";
3838 else if (value_type == "UnaryOp") // Handle negative numbers
3839 {
3840 const auto &operand = stmt["value"]["operand"];
3841 const auto &operand_type = operand["_type"];
3842 if (operand_type == "Constant")
3843 inferred_type = get_type_from_constant(operand);
3844 else if (operand_type == "Name")
3845 inferred_type = get_type_from_rhs_variable(stmt, body);
3846 else if (operand_type == "BinOp")
3847 {
3848 // Handle unary operations on binary expressions like -a ** b
3849 Json temp_stmt = {{"value", operand}};
3850 inferred_type = get_type_from_binary_expr(temp_stmt, body);
3851 }
3852 else
3853 {
3854 // Any other operand, e.g. -c.speed in a sort key (#7745): infer the
3855 // operand (an unknown one leaves the assignment to the converter), then
3856 // the operator's result. `not` is always a bool, and a numeric operator
3857 // promotes a bool to int.
3858 const Json operand_stmt = {
3859 {"_type", "Assign"}, {"value", operand}, {"lineno", current_line_}};
3860 std::string operand_inferred;
3861 const InferResult operand_result =
3862 infer_type(operand_stmt, body, operand_inferred);
3863 if (operand_result != InferResult::OK)
3864 return operand_result;
3865 if (stmt["value"]["op"]["_type"] == "Not")
3866 inferred_type = "bool";
3867 else
3868 inferred_type = operand_inferred == "bool" ? "int" : operand_inferred;
3869 }
3870 }
3871
3872 // Get type from RHS variable
3873 else if (value_type == "Name" || value_type == "Subscript")
3874 {
3875 // If RHS is a function name (e.g. g = f), skip annotation so the
3876 // converter can infer the function-pointer type directly.
3877 if (
3878 value_type == "Name" && stmt["value"].contains("id") &&
3880 ast_["body"], stmt["value"]["id"].template get<std::string>())
3881 .empty())
3882 return InferResult::UNKNOWN;
3883
3884 inferred_type = get_type_from_rhs_variable(stmt, body);
3885 if (inferred_type.empty() && value_type == "Name")
3886 return InferResult::UNKNOWN;
3887 }
3888
3889 // Get type from RHS binary expression
3890 else if (value_type == "BinOp")
3891 {
3892 std::string got_type = get_type_from_binary_expr(stmt, body);
3893 if (!got_type.empty())
3894 inferred_type = got_type;
3895 }
3896
3897 // Get type from conditional expressions (ternary operator)
3898 else if (value_type == "IfExp")
3899 {
3900 // A choice between functions is typed by the converter, as `g = f` is.
3901 if (!function_value_return_type(stmt["value"]).empty())
3902 return InferResult::UNKNOWN;
3903 return infer_type_from_ifexp(stmt["value"], body, inferred_type);
3904 }
3905
3906 // Get type from top-level functions
3907 else if (
3908 value_type == "Call" && stmt["value"]["func"]["_type"] == "Name" &&
3909 !type_utils::is_python_model_func(stmt["value"]["func"]["id"]))
3910 {
3911 inferred_type = get_type_from_call(stmt);
3912 if (inferred_type.empty())
3913 return InferResult::UNKNOWN;
3914 }
3915
3916 // Get type from methods and class methods
3917 else if (
3918 value_type == "Call" && stmt["value"]["func"]["_type"] == "Attribute")
3919 {
3920 // np.zeros/ones/full/array/... produce a concrete, fixed-shape array in
3921 // the converter, but the numpy operational model declares them as
3922 // returning the generic `list[float]` (numpy.py has no notion of the
3923 // converter's later concrete shape). That mismatch is harmless on its
3924 // own -- get_var_assign retypes the binding from the converted RHS
3925 // regardless of a stale list annotation -- except for exactly one
3926 // shape: a local variable the enclosing function then returns directly
3927 // (`a = np.zeros(3); return a`), where get_function_definition reads
3928 // this same generic list annotation from `function_node["returns"]`
3929 // *before* the body is converted and locks the function's own return
3930 // type to it (the array_return_local_* gap). Scope the decline to that
3931 // shape specifically -- ANY other numpy-constructor assignment (module
3932 // level, a parameter-feeding local, an unrelated local) keeps its usual
3933 // annotation, which other inference still depends on (e.g.
3934 // array_param_shape_metadata_success's module-level array passed into a
3935 // function that reads its shape).
3936 if (
3937 is_numpy_array_ctor_call(stmt["value"]) && stmt.contains("targets") &&
3938 stmt["targets"].is_array() && stmt["targets"].size() == 1 &&
3939 stmt["targets"][0].contains("id") &&
3940 current_func_returns_name_directly(
3941 stmt["targets"][0]["id"].template get<std::string>()))
3942 return InferResult::UNKNOWN;
3943
3944 // Try get_type_from_call first (checks builtin_functions map)
3945 inferred_type = get_type_from_call_or_random(stmt);
3946
3947 // If that didn't work, try get_type_from_method
3948 if (inferred_type.empty())
3949 inferred_type = get_type_from_method(stmt["value"]);
3950
3951 // Handle module.Class() constructor calls (e.g., datetime.datetime(...))
3952 // Use the attribute name as the type if nothing else worked
3953 if (inferred_type.empty())
3954 {
3955 const auto &func = stmt["value"]["func"];
3956 if (
3957 func.contains("attr") && func.contains("value") &&
3958 func["value"]["_type"] == "Name" && func["value"].contains("id"))
3959 {
3960 std::string attr_name = func["attr"].template get<std::string>();
3961
3962 // Only use attribute as type if it looks like a constructor call
3963 // (i.e., attribute name starts with uppercase)
3964 if (!attr_name.empty() && std::isupper(attr_name[0]))
3965 inferred_type = attr_name;
3966 // For method calls that couldn't be resolved, return UNKNOWN
3967 else
3968 return InferResult::UNKNOWN;
3969 }
3970 // Method call on subscript (e.g., nested[i].pop()): let converter infer
3971 else if (func.contains("value") && func["value"]["_type"] == "Subscript")
3972 return InferResult::UNKNOWN;
3973 // Method call on temporary instance (e.g., A().f()): let converter infer
3974 // if get_type_from_method couldn't resolve (class not in AST etc.)
3975 else if (func.contains("value") && func["value"]["_type"] == "Call")
3976 return InferResult::UNKNOWN;
3977 // Method call on nested attribute chain (e.g., a.b.c.f()): could not
3978 // resolve type - let converter infer
3979 else if (func.contains("value") && func["value"]["_type"] == "Attribute")
3980 return InferResult::UNKNOWN;
3981 }
3982 }
3983 else
3984 return InferResult::UNKNOWN;
3985
3986 return InferResult::OK;
3987}
3988
3989// ---------- AST walkers and lookup ----------
3990
3994template <class Json>
3995bool python_annotation<Json>::is_rebound_in_scope(const std::string &var_name)
3996{
3997 // has_multiple_assignments_in_block takes the enclosing *node*, not its
3998 // body array -- it reads block["body"] itself.
3999 const Json func =
4000 json_utils::try_find_function(ast_["body"], get_current_func_name());
4001 if (
4002 !func.empty() &&
4004 return true;
4005
4006 return json_utils::has_multiple_assignments_in_block(var_name, ast_);
4007}
4008
4009template <class Json>
4010Json python_annotation<Json>::find_var_node_for_inference(
4011 const std::string &var_name)
4012{
4013 Json var_node =
4014 json_utils::find_var_decl(var_name, get_current_func_name(), ast_);
4015
4016 if (
4017 !has_annotation(var_node) &&
4018 (var_node.empty() ||
4019 (var_node.contains("_type") && var_node["_type"] == "arg")) &&
4020 parent_func != nullptr)
4021 {
4022 if (
4023 (*parent_func).contains("args") &&
4024 (*parent_func)["args"].contains("args"))
4025 {
4026 var_node =
4027 find_annotated_assign(var_name, (*parent_func)["args"]["args"]);
4028 // Also search keyword-only args
4029 if (var_node.empty() && (*parent_func)["args"].contains("kwonlyargs"))
4030 var_node =
4031 find_annotated_assign(var_name, (*parent_func)["args"]["kwonlyargs"]);
4032 }
4033
4034 if (!has_annotation(var_node) && (*parent_func).contains("body"))
4035 var_node = find_annotated_assign(var_name, (*parent_func)["body"]);
4036 }
4037
4038 if (
4039 !has_annotation(var_node) &&
4040 (var_node.empty() ||
4041 (var_node.contains("_type") && var_node["_type"] == "arg")))
4042 {
4043 Json global_var_node = json_utils::find_var_decl(var_name, "", ast_);
4044 if (!global_var_node.empty())
4045 var_node = global_var_node;
4046 }
4047
4048 if (!has_annotation(var_node) && var_node.empty())
4049 var_node = find_annotated_assign(var_name, ast_["body"]);
4050
4051 return var_node;
4052}
4053
4054template <class Json>
4055bool python_annotation<Json>::append_arg_is_plain_int(const Json &arg)
4056{
4057 const std::string type = arg.value("_type", std::string());
4058
4059 if (type == "Constant")
4060 return get_type_from_constant(arg) == "int";
4061
4062 if (type == "BinOp" || type == "UnaryOp")
4063 return get_argument_type(arg) == "int";
4064
4065 if (type == "Name")
4066 {
4067 Json var_node = find_var_node_for_inference(arg.value("id", std::string()));
4068 // An int annotation (e.g. a ``for n in range(...)`` target the
4069 // annotation pass typed as ``n: int``) is trustworthy.
4070 if (has_annotation(var_node))
4071 return json_utils::get_annotation_type_name(var_node["annotation"]) ==
4072 "int";
4073 // Otherwise follow the binding's RHS; a name bound to a subscript,
4074 // attribute, or call lands on the default branch below and is rejected.
4075 if (var_node.contains("value") && var_node["value"].is_object())
4076 return append_arg_is_plain_int(var_node["value"]);
4077 return false;
4078 }
4079
4080 // Subscript / Attribute / Call / ... -- indirection, do not trust.
4081 return false;
4082}
4083
4084template <class Json>
4085bool python_annotation<Json>::expr_calls_function(
4086 const Json &node,
4087 const std::string &func_name)
4088{
4089 if (node.is_object())
4090 {
4091 auto type_it = node.find("_type");
4092 if (type_it != node.end() && *type_it == "Call")
4093 {
4094 auto func_it = node.find("func");
4095 if (func_it != node.end() && func_it->is_object())
4096 {
4097 auto ft = func_it->find("_type");
4098 auto fid = func_it->find("id");
4099 if (
4100 ft != func_it->end() && *ft == "Name" && fid != func_it->end() &&
4101 *fid == func_name)
4102 return true;
4103 }
4104 }
4105 for (auto it = node.begin(); it != node.end(); ++it)
4106 if (expr_calls_function(*it, func_name))
4107 return true;
4108 }
4109 else if (node.is_array())
4110 {
4111 for (const Json &child : node)
4112 if (expr_calls_function(child, func_name))
4113 return true;
4114 }
4115 return false;
4116}
4117
4118template <class Json>
4119bool python_annotation<Json>::arithmetic_reads_attribute(const Json &node)
4120{
4121 const std::string type = node.value("_type", std::string());
4122 if (type == "Attribute")
4123 return true;
4124 if (type == "BinOp")
4125 return arithmetic_reads_attribute(node["left"]) ||
4126 arithmetic_reads_attribute(node["right"]);
4127 return type == "UnaryOp" && arithmetic_reads_attribute(node["operand"]);
4128}
4129
4131static const char *const untyped_name_return = "?";
4132
4135static bool all_pointer_sized_scalars(const std::set<std::string> &types)
4136{
4137 static const std::set<std::string> scalars = {"bool", "int", "str"};
4138 return !types.empty() &&
4139 std::all_of(types.begin(), types.end(), [](const std::string &t) {
4140 return scalars.count(t) != 0;
4141 });
4142}
4143
4144template <class Json>
4145void python_annotation<Json>::collect_return_types(
4146 const Json &body,
4147 const std::string &func_name,
4148 std::set<std::string> &types)
4149{
4150 for (const Json &stmt : body)
4151 {
4152 // A nested function or class owns its own `return` statements: Python
4153 // scopes them to the nested definition, so they must not leak into the
4154 // enclosing function's inferred return type. Without this guard, an
4155 // unannotated function whose body declares a nested helper that returns a
4156 // different type (e.g. a `check()` returning bool) would wrongly inherit
4157 // that type, mis-categorising the enclosing return and breaking downstream
4158 // type-dependent logic such as the cross-type `==` fold (GitHub #4807).
4159 const std::string &stmt_type = stmt["_type"];
4160 if (
4161 stmt_type == "FunctionDef" || stmt_type == "AsyncFunctionDef" ||
4162 stmt_type == "ClassDef")
4163 continue;
4164
4165 // Found a return statement
4166 if (stmt["_type"] == "Return" && !stmt["value"].is_null())
4167 {
4168 const Json &return_val = stmt["value"];
4169
4170 // Skip self-recursive returns. A bare `return f(...)` or a compound
4171 // expression embedding a recursive call (e.g. `return f(n-1) + f(n-2)`)
4172 // would re-enter return-type inference for `f` while `f` is still under
4173 // analysis, recursing until the stack overflows. The base-case returns
4174 // supply the type; the recursive combination cannot add a new one.
4175 if (expr_calls_function(return_val, func_name))
4176 continue;
4177
4178 // Handle function calls (including nested functions)
4179 if (
4180 return_val["_type"] == "Call" && return_val.contains("func") &&
4181 return_val["func"]["_type"] == "Name")
4182 {
4183 const std::string &called_func = return_val["func"]["id"];
4184
4185 // get_function_return_type has no access to this call's arguments.
4186 const std::string arg_dependent = arg_dependent_call_type(return_val);
4187 if (!arg_dependent.empty())
4188 {
4189 types.insert(arg_dependent);
4190 continue;
4191 }
4192
4193 // Try to get the return type of the called function
4194 try
4195 {
4196 std::string called_func_type =
4197 get_function_return_type(called_func, ast_);
4198 if (!called_func_type.empty() && called_func_type != "NoneType")
4199 {
4200 types.insert(called_func_type);
4201 continue;
4202 }
4203 }
4204 catch (std::runtime_error &)
4205 {
4206 // Not a function: a variable or parameter holding one (#8669).
4207 const std::string held =
4208 function_value_return_type(return_val["func"], &return_val);
4209 if (!held.empty())
4210 {
4211 types.insert(held);
4212 continue;
4213 }
4214 }
4215 }
4216
4217 // Method calls that always return a list (e.g. `s.split()`). These have
4218 // an Attribute func, so the Name-call branch above misses them, and
4219 // get_argument_type cannot type the result when the receiver is an
4220 // unannotated parameter -- it returns "". Dropping the list branch lets a
4221 // sibling scalar return (e.g. `return len(s)`) annotate the whole
4222 // function as that scalar, which mistypes the list path and makes its
4223 // call result const-fold to a bogus size-1 list. Recording "list" here
4224 // means a mixed list+scalar body collects {list, scalar}, which
4225 // infer_from_return_statements leaves unannotated (distinct bases) so the
4226 // converter's per-RETURN inference types each path correctly.
4227 if (
4228 return_val["_type"] == "Call" && return_val.contains("func") &&
4229 return_val["func"]["_type"] == "Attribute" &&
4230 return_val["func"].contains("attr") &&
4231 get_string_method_return_type(return_val["func"]["attr"]) == "list")
4232 {
4233 types.insert("list");
4234 continue;
4235 }
4236
4237 // Reuse get_argument_type to infer the return value type. "Any" for
4238 // arithmetic over an attribute read only means the attribute's type is
4239 // unknown here. The converter types the result from the class's member
4240 // types, whereas an "Any" return would make it a double (#8942).
4241 std::string inferred_type = get_argument_type(return_val);
4242 if (inferred_type.empty() && return_val["_type"] == "Name")
4243 types.insert(untyped_name_return);
4244 if (
4245 !inferred_type.empty() &&
4246 (inferred_type != "Any" || !arithmetic_reads_attribute(return_val)))
4247 types.insert(inferred_type);
4248 }
4249
4250 // Recursively check nested blocks (if/while/for/try)
4251 if (stmt.contains("body") && stmt["body"].is_array())
4252 collect_return_types(stmt["body"], func_name, types);
4253
4254 if (stmt.contains("orelse") && stmt["orelse"].is_array())
4255 collect_return_types(stmt["orelse"], func_name, types);
4256 }
4257}
4258
4259template <class Json>
4260std::string python_annotation<Json>::infer_from_return_statements(
4261 const Json &body,
4262 const std::string &func_name,
4263 bool *divergent)
4264{
4265 std::set<std::string> types;
4266 collect_return_types(body, func_name, types);
4267 const bool returns_untyped_name = types.erase(untyped_name_return) != 0;
4268
4269 // Drop non-informative markers when concrete return types are also present.
4270 // - NoneType: a `return None` alongside value returns marks the function as
4271 // Optional; that optionality is handled separately by the caller's
4272 // has_return_none() check (mixed value+None -> Optional wrapping), so it
4273 // must not suppress the value-type inference (e.g. `return 0` /
4274 // `return None` must still infer `int`, GitHub #3563).
4275 // - Any: an under-specified branch (e.g. `return arr` for a not-yet-typed
4276 // parameter) carries no type information and must not veto a sibling
4277 // branch that does (GitHub quixbugs mergesort).
4278 // Both are kept only when sole, so a function returning just None/Any is
4279 // still annotated.
4280 if (types.size() > 1)
4281 {
4282 types.erase("NoneType");
4283 types.erase("Any");
4284 }
4285
4286 // A name of unknown type, e.g. an unannotated parameter no call site types
4287 // (#9069), may hold any value, so a sibling scalar return does not type it.
4288 if (returns_untyped_name && all_pointer_sized_scalars(types))
4289 {
4290 if (divergent)
4291 *divergent = true;
4292 return "";
4293 }
4294
4295 // Exactly one inferred return type: narrow the function to it.
4296 if (types.size() == 1)
4297 return *types.begin();
4298
4299 if (types.empty())
4300 return "";
4301
4302 // Branches return different numeric-tower types (e.g. `int` on one path and
4303 // `float` on another). Python promotes within bool < int < float < complex,
4304 // so widen to the broadest present rather than leaving it unannotated. This
4305 // matches the int->float promotion the ternary (IfExp) inference already
4306 // applies and keeps numeric functions concretely typed.
4307 {
4308 static const std::map<std::string, int> numeric_rank = {
4309 {"bool", 0}, {"int", 1}, {"float", 2}, {"complex", 3}};
4310 std::string widest;
4311 int best = -1;
4312 for (const std::string &t : types)
4313 {
4314 auto it = numeric_rank.find(t);
4315 if (it == numeric_rank.end())
4316 {
4317 widest.clear();
4318 break;
4319 }
4320 if (it->second > best)
4321 {
4322 best = it->second;
4323 widest = t;
4324 }
4325 }
4326 if (!widest.empty())
4327 return widest;
4328 }
4329
4330 // All remaining types share one collection base (e.g. `list` / `list[int]` /
4331 // `list[list]`, or `dict` / `dict[...]`): they are the same kind of container
4332 // and differ only in how precisely the element is known. Returning "" here
4333 // would strip the collection-ness and break callers that iterate the result
4334 // (quixbugs mergesort/subsequences). When exactly one element-typed
4335 // ("bracketed") form is present — e.g. {list, list[int]} from a bare list
4336 // literal alongside a typed one — return it so comprehensions and element
4337 // access still see the element type. When the branches disagree on the
4338 // element type itself (e.g. {list[int], list[str]}), fall back to the bare
4339 // base: still iterable, without asserting a false element type.
4340 {
4341 auto base_of = [](const std::string &t) {
4342 auto p = t.find('[');
4343 return p == std::string::npos ? t : t.substr(0, p);
4344 };
4345 const std::string base = base_of(*types.begin());
4346 std::string bracketed;
4347 bool same_base = true, conflicting_elements = false;
4348 for (const std::string &t : types)
4349 {
4350 if (base_of(t) != base)
4351 {
4352 same_base = false;
4353 break;
4354 }
4355 if (t != base) // an element-typed form, e.g. list[int]
4356 {
4357 if (!bracketed.empty() && bracketed != t)
4358 conflicting_elements = true;
4359 bracketed = t;
4360 }
4361 }
4362 if (same_base)
4363 return (bracketed.empty() || conflicting_elements) ? base : bracketed;
4364 }
4365
4366 // Either no return value could be typed, or distinct branches return
4367 // incompatible types (e.g. a `return -1` sentinel alongside a `return
4368 // bin(...)` string, the common HumanEval shape). Leaving the function
4369 // unannotated keeps the converter from narrowing it to one branch's type,
4370 // which would otherwise make the cross-type `==`/`!=` fold collapse a valid
4371 // comparison against the other branch's type to a constant False
4372 // (GitHub #5157). Mirrors the mixed value+None -> Optional handling above.
4373 if (divergent)
4374 *divergent = true;
4375 return "";
4376}
4377
4378template <class Json>
4379const Json python_annotation<Json>::find_annotated_assign(
4380 const std::string &node_name,
4381 const Json &body)
4382{
4383 for (const Json &elem : body)
4384 {
4385 // Check if this element is the variable we're looking for
4386 if (
4387 elem.contains("_type") &&
4388 ((elem["_type"] == "AnnAssign" && elem.contains("target") &&
4389 elem["target"].contains("id") &&
4390 elem["target"]["id"].template get<std::string>() == node_name) ||
4391 (elem["_type"] == "Assign" && elem.contains("targets") &&
4392 elem["targets"].is_array() && !elem["targets"].empty() &&
4393 elem["targets"][0].contains("_type") &&
4394 elem["targets"][0]["_type"] == "Name" &&
4395 elem["targets"][0].contains("id") &&
4396 elem["targets"][0]["id"].template get<std::string>() == node_name) ||
4397 (elem["_type"] == "arg" && elem["arg"] == node_name)))
4398 {
4399 return elem;
4400 }
4401
4402 // Tuple/list unpacking assignment (`A, B = rhs`): synthesise an
4403 // AnnAssign so callers reading node["annotation"]["id"] resolve the
4404 // destructured element's type (GitHub #4532).
4405 if (
4406 Json synth = match_unpacking_assignment(elem, node_name); !synth.empty())
4407 return synth;
4408
4409 // Recursively search inside nested blocks
4410 if (elem.contains("_type"))
4411 {
4412 const std::string &elem_type = elem["_type"];
4413
4414 // Search inside If blocks (both if and else branches)
4415 if (elem_type == "If")
4416 {
4417 if (elem.contains("body") && !elem["body"].empty())
4418 {
4419 Json result = find_annotated_assign(node_name, elem["body"]);
4420 if (!result.empty())
4421 return result;
4422 }
4423 if (elem.contains("orelse") && !elem["orelse"].empty())
4424 {
4425 Json result = find_annotated_assign(node_name, elem["orelse"]);
4426 if (!result.empty())
4427 return result;
4428 }
4429 }
4430 // Search inside While blocks
4431 else if (elem_type == "While")
4432 {
4433 if (elem.contains("body") && !elem["body"].empty())
4434 {
4435 Json result = find_annotated_assign(node_name, elem["body"]);
4436 if (!result.empty())
4437 return result;
4438 }
4439 }
4440 // Search inside For blocks
4441 else if (elem_type == "For")
4442 {
4443 if (elem.contains("body") && !elem["body"].empty())
4444 {
4445 Json result = find_annotated_assign(node_name, elem["body"]);
4446 if (!result.empty())
4447 return result;
4448 }
4449 }
4450 // Search inside Try blocks
4451 else if (elem_type == "Try")
4452 {
4453 if (elem.contains("body") && !elem["body"].empty())
4454 {
4455 Json result = find_annotated_assign(node_name, elem["body"]);
4456 if (!result.empty())
4457 return result;
4458 }
4459 if (elem.contains("handlers") && !elem["handlers"].empty())
4460 {
4461 for (const Json &handler : elem["handlers"])
4462 {
4463 if (handler.contains("body") && !handler["body"].empty())
4464 {
4465 Json result = find_annotated_assign(node_name, handler["body"]);
4466 if (!result.empty())
4467 return result;
4468 }
4469 }
4470 }
4471 }
4472 }
4473 }
4474 return Json();
4475}
4476
4477template <class Json>
4478Json python_annotation<Json>::find_receiver_decl(const std::string &obj)
4479{
4480 Json node = json_utils::find_var_decl(obj, get_current_func_name(), ast_);
4481 if (!node.empty())
4482 return node;
4483 // A name bound only by unpacking (`a, b = f()`) has no plain assignment.
4484 const Json &body =
4485 current_func != nullptr ? (*current_func)["body"] : ast_["body"];
4486 return find_annotated_assign(obj, body);
4487}
4488
4489template <class Json>
4490Json python_annotation<Json>::match_unpacking_assignment(
4491 const Json &elem,
4492 const std::string &node_name)
4493{
4494 if (
4495 !elem.contains("_type") || elem["_type"] != "Assign" ||
4496 !elem.contains("targets") || !elem["targets"].is_array() ||
4497 elem["targets"].empty())
4498 return Json();
4499 const Json &target = elem["targets"][0];
4500 if (
4501 !target.is_object() || !target.contains("_type") ||
4502 (target["_type"] != "Tuple" && target["_type"] != "List") ||
4503 !target.contains("elts") || !target["elts"].is_array())
4504 return Json();
4505
4506 const Json &elts = target["elts"];
4507 for (size_t i = 0; i < elts.size(); ++i)
4508 {
4509 if (
4510 !elts[i].is_object() || !elts[i].contains("_type") ||
4511 elts[i]["_type"] != "Name" || !elts[i].contains("id") ||
4512 elts[i]["id"] != node_name)
4513 continue;
4514 std::string elem_type = elem.contains("value")
4515 ? infer_unpacked_element_type(elem["value"], i)
4516 : "Any";
4517 return Json{
4518 {"_type", "AnnAssign"},
4519 {"target", Json{{"_type", "Name"}, {"id", node_name}}},
4520 {"annotation", Json{{"_type", "Name"}, {"id", elem_type}}},
4521 {"lineno", elem.value("lineno", 0)}};
4522 }
4523 return Json();
4524}
4525
4526// ---------- per-class subscript-key tuple inference ----------
4527
4528template <class Json>
4529std::vector<std::string>
4530python_annotation<Json>::infer_subscript_key_tuple_types(
4531 const std::string &class_name)
4532{
4533 // Unanimous-wins on per-position element types. The first qualifying
4534 // site fixes the expected type vector; any later site that disagrees on
4535 // arity or on a per-position type abandons synthesis so the existing
4536 // void* default keeps working for those call sites.
4537 bool any_reject = false;
4538 std::vector<std::string> expected;
4539 auto reducer = [&](const Json &elts, const Json *enclosing_func) {
4540 if (any_reject)
4541 return;
4542 std::vector<std::string> types =
4543 infer_tuple_element_types(elts, enclosing_func);
4544 if (types.empty())
4545 {
4546 any_reject = true;
4547 return;
4548 }
4549 if (expected.empty())
4550 expected = std::move(types);
4551 else if (expected != types)
4552 any_reject = true;
4553 };
4554
4555 auto scan = [&](const Json &ast) {
4556 std::unordered_map<std::string, std::string> var_to_class;
4557 collect_class_instance_map(ast, var_to_class);
4558 visit_class_tuple_subscripts(ast, class_name, var_to_class, reducer);
4559 };
4560
4561 scan(ast_);
4562 for (const Json *extra : extra_subscript_inference_asts_)
4563 scan(*extra);
4564
4565 if (any_reject)
4566 return {};
4567 return expected;
4568}
4569
4570template <class Json>
4571void python_annotation<Json>::collect_class_instance_map(
4572 const Json &ast,
4573 std::unordered_map<std::string, std::string> &out) const
4574{
4575 auto record = [&](const Json &target_id, const Json &annotation) {
4576 if (
4577 !target_id.is_string() || !annotation.is_object() ||
4578 !annotation.contains("_type") || annotation["_type"] != "Name" ||
4579 !annotation.contains("id") || !annotation["id"].is_string())
4580 return;
4581 out[target_id.template get<std::string>()] =
4582 annotation["id"].template get<std::string>();
4583 };
4584
4585 std::function<void(const Json &)> walk = [&](const Json &node) {
4586 if (node.is_object())
4587 {
4588 if (
4589 node.contains("_type") && node["_type"] == "AnnAssign" &&
4590 node.contains("target") && node["target"].is_object() &&
4591 node["target"].contains("_type") && node["target"]["_type"] == "Name" &&
4592 node["target"].contains("id") && node.contains("annotation"))
4593 record(node["target"]["id"], node["annotation"]);
4594 else if (
4595 node.contains("_type") && node["_type"] == "arg" &&
4596 node.contains("arg") && node.contains("annotation"))
4597 record(node["arg"], node["annotation"]);
4598 for (auto it = node.begin(); it != node.end(); ++it)
4599 walk(it.value());
4600 }
4601 else if (node.is_array())
4602 {
4603 for (const auto &elem : node)
4604 walk(elem);
4605 }
4606 };
4607
4608 if (ast.is_object() && ast.contains("body"))
4609 walk(ast["body"]);
4610}
4611
4612template <class Json>
4613template <typename Visitor>
4614void python_annotation<Json>::visit_class_tuple_subscripts(
4615 const Json &node,
4616 const std::string &class_name,
4617 const std::unordered_map<std::string, std::string> &var_to_class,
4618 Visitor &&visit,
4619 const Json *enclosing_func)
4620{
4621 if (node.is_object())
4622 {
4623 const Json *next_enclosing = enclosing_func;
4624 if (
4625 node.contains("_type") &&
4626 (node["_type"] == "FunctionDef" || node["_type"] == "AsyncFunctionDef"))
4627 next_enclosing = &node;
4628
4629 if (
4630 node.contains("_type") && node["_type"] == "Subscript" &&
4631 node.contains("value") && node["value"].is_object() &&
4632 node["value"].contains("_type") && node["value"]["_type"] == "Name" &&
4633 node["value"].contains("id") && node.contains("slice") &&
4634 node["slice"].is_object() && node["slice"].contains("_type") &&
4635 node["slice"]["_type"] == "Tuple" && node["slice"].contains("elts") &&
4636 node["slice"]["elts"].is_array() && !node["slice"]["elts"].empty())
4637 {
4638 auto it =
4639 var_to_class.find(node["value"]["id"].template get<std::string>());
4640 if (it != var_to_class.end() && it->second == class_name)
4641 visit(node["slice"]["elts"], next_enclosing);
4642 }
4643 for (auto it = node.begin(); it != node.end(); ++it)
4644 visit_class_tuple_subscripts(
4645 it.value(), class_name, var_to_class, visit, next_enclosing);
4646 }
4647 else if (node.is_array())
4648 {
4649 for (const auto &elem : node)
4650 visit_class_tuple_subscripts(
4651 elem, class_name, var_to_class, visit, enclosing_func);
4652 }
4653}
4654
4655template <class Json>
4656std::string python_annotation<Json>::resolve_name_in_enclosing_func(
4657 const std::string &name,
4658 const Json &enclosing_func)
4659{
4660 auto lookup = [&](const Json &scope) -> std::string {
4661 Json node = find_annotated_assign(name, scope);
4662 if (has_annotation(node))
4663 return json_utils::get_annotation_type_name(node["annotation"]);
4664 return "";
4665 };
4666
4667 if (enclosing_func.contains("args"))
4668 {
4669 const Json &arg_spec = enclosing_func["args"];
4670 for (const char *key : {"args", "kwonlyargs"})
4671 if (arg_spec.contains(key))
4672 if (std::string t = lookup(arg_spec[key]); !t.empty())
4673 return t;
4674 }
4675 if (enclosing_func.contains("body"))
4676 return lookup(enclosing_func["body"]);
4677 return "";
4678}
4679
4680template <class Json>
4681std::string python_annotation<Json>::resolve_wildcard_import_func(
4682 const std::string &func_name)
4683{
4684 if (!module_manager_ || !ast_.contains("body"))
4685 return "";
4686 for (const auto &node : ast_["body"])
4687 {
4688 if (
4689 !node.contains("_type") || node["_type"] != "ImportFrom" ||
4690 !node.contains("names") || !node.contains("module") ||
4691 node["module"].is_null())
4692 continue;
4693 bool is_star = false;
4694 for (const auto &name : node["names"])
4695 if (name.contains("name") && name["name"] == "*")
4696 {
4697 is_star = true;
4698 break;
4699 }
4700 if (!is_star)
4701 continue;
4702 auto module = module_manager_->get_module(node["module"]);
4703 if (!module || !module->star_import_binds(func_name))
4704 continue;
4705 auto func_info = module->get_function(func_name);
4706 if (func_info.name_.empty())
4707 continue;
4708 if (func_info.return_type_.empty() || func_info.return_type_ == "NoneType")
4709 return func_name;
4710 return func_info.return_type_;
4711 }
4712 return "";
4713}
4714
4715template <class Json>
4716bool python_annotation<Json>::reads_import(
4717 const std::string &name,
4718 const Json &use) const
4719{
4720 if (parent_func)
4721 return false;
4722 for (const auto &node : ast_["body"])
4723 {
4724 const Json names = node.value("names", Json::array());
4725 if (
4726 node.value("_type", "") != "ImportFrom" ||
4727 std::none_of(names.begin(), names.end(), [](const Json &alias) {
4728 return alias.value("name", "") == "*";
4729 }))
4730 continue;
4731 const Json from = node.value("module", Json());
4732 const auto module =
4733 module_manager_ && from.is_string()
4734 ? module_manager_->get_module(from.template get<std::string>())
4735 : nullptr;
4736 if (
4737 !module || !module->star_import_binds(name) ||
4738 module->get_function(name).name_.empty())
4739 return false;
4740 }
4741 try
4742 {
4744 ast_,
4746 current_func,
4747 !current_class_name_.empty(),
4748 current_func == nullptr,
4749 name,
4750 use)
4751 .value_or(false);
4752 }
4753 catch (const std::runtime_error &)
4754 {
4755 return false;
4756 }
4757}
4758
4759template <class Json>
4760std::string python_annotation<Json>::resolve_expr_in_enclosing_func(
4761 const Json &elt,
4762 const Json &enclosing_func)
4763{
4764 if (!elt.contains("_type"))
4765 return "";
4766 const std::string &t = elt["_type"];
4767 if (t == "Constant")
4768 return get_type_from_constant(elt);
4769 if (t == "Name" && elt.contains("id"))
4770 return resolve_name_in_enclosing_func(elt["id"], enclosing_func);
4771 if (t == "UnaryOp" && elt.contains("operand"))
4772 return resolve_expr_in_enclosing_func(elt["operand"], enclosing_func);
4773 if (t == "BinOp" && elt.contains("left") && elt.contains("right"))
4774 {
4775 std::string l = resolve_expr_in_enclosing_func(elt["left"], enclosing_func);
4776 std::string r =
4777 resolve_expr_in_enclosing_func(elt["right"], enclosing_func);
4778 if (l.empty() || r.empty())
4779 return "";
4780 if (l == "float" || r == "float")
4781 return "float";
4782 if (l == "int" && r == "int")
4783 return "int";
4784 return "";
4785 }
4786 return "";
4787}
4788
4789template <class Json>
4790std::vector<std::string> python_annotation<Json>::infer_tuple_element_types(
4791 const Json &elts,
4792 const Json *enclosing_func)
4793{
4794 std::vector<std::string> types;
4795 types.reserve(elts.size());
4796 for (const Json &elt : elts)
4797 {
4798 if (!elt.contains("_type"))
4799 return {};
4800 const std::string &t = elt["_type"];
4801 if (t == "Slice")
4802 {
4803 types.emplace_back("slice");
4804 continue;
4805 }
4806 if (
4807 t == "Call" && elt.contains("func") && elt["func"].is_object() &&
4808 elt["func"].contains("_type") && elt["func"]["_type"] == "Name" &&
4809 elt["func"].contains("id") && elt["func"]["id"] == "slice")
4810 {
4811 types.emplace_back("slice");
4812 continue;
4813 }
4814 std::string resolved;
4815 if (enclosing_func != nullptr)
4816 resolved = resolve_expr_in_enclosing_func(elt, *enclosing_func);
4817 if (resolved.empty())
4818 resolved = get_argument_type(elt);
4819 if (resolved.empty() || resolved == "Any")
4820 return {};
4821 types.push_back(std::move(resolved));
4822 }
4823 return types;
4824}
4825
4826// ---------- function-call collection and parameter inference ----------
4827
4828template <class Json>
4829std::vector<Json> python_annotation<Json>::find_function_calls(
4830 const std::string &func_name,
4831 const Json &body)
4832{
4833 std::vector<Json> calls;
4834 find_function_calls_recursive(func_name, body, calls);
4835 return calls;
4836}
4837
4838template <class Json>
4839void python_annotation<Json>::find_function_calls_recursive(
4840 const std::string &func_name,
4841 const Json &node,
4842 std::vector<Json> &calls)
4843{
4844 if (node.is_object())
4845 {
4846 // Check if this is a function call to our target function
4847 if (
4848 node.contains("_type") && node["_type"] == "Call" &&
4849 node.contains("func") && node["func"]["_type"] == "Name" &&
4850 node["func"]["id"] == func_name)
4851 {
4852 calls.push_back(node);
4853 }
4854
4855 // Recursively search all fields
4856 for (auto it = node.begin(); it != node.end(); ++it)
4857 find_function_calls_recursive(func_name, it.value(), calls);
4858 }
4859 else if (node.is_array())
4860 {
4861 for (const auto &element : node)
4862 find_function_calls_recursive(func_name, element, calls);
4863 }
4864}
4865
4866template <class Json>
4867std::vector<std::string>
4868python_annotation<Json>::collect_parameter_types_from_calls(
4869 size_t param_index,
4870 const std::vector<Json> &function_calls)
4871{
4872 std::vector<std::string> types;
4873 std::unordered_set<std::string> seen;
4874
4875 for (const Json &call : function_calls)
4876 {
4877 if (!call.contains("args") || param_index >= call["args"].size())
4878 continue;
4879
4880 std::string arg_type = get_argument_type(call["args"][param_index]);
4881 if (arg_type.empty())
4882 continue;
4883
4884 if (seen.insert(arg_type).second)
4885 types.push_back(arg_type);
4886 }
4887
4888 return types;
4889}
4890
4891template <class Json>
4892std::vector<std::string>
4893python_annotation<Json>::infer_tuple_param_types_from_calls(
4894 size_t param_index,
4895 const std::vector<Json> &function_calls)
4896{
4897 std::vector<std::string> result;
4898 bool initialized = false;
4899
4900 for (const Json &call : function_calls)
4901 {
4902 if (!call.contains("args") || param_index >= call["args"].size())
4903 continue;
4904
4905 const Json &arg = call["args"][param_index];
4906 if (
4907 !arg.contains("_type") || arg["_type"] != "Tuple" ||
4908 !arg.contains("elts"))
4909 return {};
4910
4911 std::vector<std::string> shape;
4912 shape.reserve(arg["elts"].size());
4913 for (const Json &elt : arg["elts"])
4914 {
4915 std::string t = get_argument_type(elt);
4916 // Empty result and bare container kinds without parameters all
4917 // resolve to incomplete/empty types downstream. Clamp them to "Any"
4918 // so the synthesised struct never carries an empty_typet() component.
4919 if (t.empty() || t == "tuple" || t == "list" || t == "dict")
4920 t = "Any";
4921 shape.push_back(std::move(t));
4922 }
4923
4924 if (!initialized)
4925 {
4926 result = std::move(shape);
4927 initialized = true;
4928 }
4929 else
4930 {
4931 if (shape.size() != result.size())
4932 return {};
4933 for (size_t k = 0; k < result.size(); ++k)
4934 if (result[k] != shape[k])
4935 result[k] = "Any";
4936 }
4937 }
4938
4939 return result;
4940}
4941
4945template <class Json>
4947 const std::string &name,
4948 const std::string &ctx,
4949 const Json &ast)
4950{
4951 const std::vector<std::string> path = json_utils::split_function_path(ctx);
4952 for (size_t depth = path.size(); depth > 0; --depth)
4953 {
4954 const Json func = json_utils::find_function_by_path(
4955 ast, std::vector<std::string>(path.begin(), path.begin() + depth));
4956 const Json decl = json_utils::find_var_in_function(name, func);
4957 if (decl.empty())
4958 continue;
4959 if (decl.value("_type", "") == "arg")
4960 return !json_utils::get_var_node(name, func).empty();
4962 }
4963 return false;
4964}
4965
4968template <class Json>
4969std::string python_annotation<Json>::caller_arg_type(
4970 const Json &arg,
4971 const caller_scopet &scope)
4972{
4973 if (scope.in_lambda)
4974 return "";
4975 if (
4976 arg.value("_type", "") == "Name" &&
4977 rebound_in_function(arg["id"].template get<std::string>(), scope.ctx, ast_))
4978 return "";
4979 const std::string saved_ctx = current_func_name_context_;
4980 current_func_name_context_ = scope.ctx;
4981 const std::string type = get_argument_type(arg);
4982 current_func_name_context_ = saved_ctx;
4983 return type;
4984}
4985
4992template <class Json>
4993void python_annotation<Json>::collect_caller_arg_types(
4994 const std::string &callee,
4995 size_t index,
4996 const Json &node,
4997 const caller_scopet &scope,
4998 std::vector<std::string> &types)
4999{
5000 if (node.is_array())
5001 {
5002 for (const auto &elem : node)
5003 collect_caller_arg_types(callee, index, elem, scope, types);
5004 return;
5005 }
5006 if (!node.is_object())
5007 return;
5008
5009 caller_scopet inner = scope;
5010 const std::string kind = node.value("_type", "");
5011 if (kind == "ClassDef" && scope.ctx.empty())
5012 inner.class_prefix = node.value("name", "") + "@C@";
5013 else if (kind == "FunctionDef" || kind == "AsyncFunctionDef")
5014 {
5015 const std::string name = node.value("name", "");
5016 inner.ctx =
5017 scope.ctx.empty() ? scope.class_prefix + name : scope.ctx + "@F@" + name;
5018 inner.class_prefix.clear();
5019 }
5020 else if (kind == "Lambda")
5021 inner.in_lambda = true;
5022 else if (
5023 kind == "Call" && scope.ctx != scope.root && node.contains("args") &&
5024 index < node["args"].size() && node["func"].value("_type", "") == "Name" &&
5025 node["func"].value("id", "") == callee)
5026 {
5027 const std::string type = caller_arg_type(node["args"][index], scope);
5028 const std::string callee_path =
5029 scope.root.empty() ? callee : scope.root + "@F@" + callee;
5030 if (!type.empty() || scope.ctx != callee_path)
5031 types.push_back(type);
5032 }
5033
5034 for (auto it = node.begin(); it != node.end(); ++it)
5035 collect_caller_arg_types(callee, index, it.value(), inner, types);
5036}
5037
5040template <class Json>
5041std::vector<std::string> python_annotation<Json>::caller_arg_types(
5042 const std::string &func_name,
5043 size_t index,
5044 const Json &search_context)
5045{
5046 caller_scopet scope;
5047 scope.root = current_func_name_context_;
5048 scope.ctx = scope.root;
5049 std::vector<std::string> types;
5050 collect_caller_arg_types(func_name, index, search_context, scope, types);
5051 return types;
5052}
5053
5057static bool call_site_types_agree(const std::string &a, const std::string &b)
5058{
5059 if (a == b)
5060 return true;
5061 const size_t a_bracket = a.find('[');
5062 const size_t b_bracket = b.find('[');
5063 return (a_bracket == std::string::npos || b_bracket == std::string::npos) &&
5064 a.substr(0, a_bracket) == b.substr(0, b_bracket);
5065}
5066
5070template <class Json>
5071std::string python_annotation<Json>::infer_parameter_type_at_call_sites(
5072 const std::string &func_name,
5073 size_t param_index,
5074 const std::vector<Json> &function_calls,
5075 const Json &search_context)
5076{
5077 std::string inferred =
5078 infer_parameter_type_from_calls(param_index, function_calls);
5079 const std::vector<std::string> callers =
5080 caller_arg_types(func_name, param_index, search_context);
5081 // Every call is made in another function, whose scope alone can type its
5082 // argument (`find(pkt.ident)` with `pkt` a caller's local, #8672).
5083 if (
5084 inferred.empty() && !callers.empty() &&
5085 callers.size() == function_calls.size())
5086 inferred = callers.front();
5087 for (const std::string &type : callers)
5088 if (!call_site_types_agree(type, inferred))
5089 return "";
5090 return inferred;
5091}
5092
5096template <class Json>
5097bool python_annotation<Json>::specializable_at_call_sites(
5098 const std::string &func_name,
5099 size_t param_index,
5100 std::vector<std::string> &call_types,
5101 const Json &search_context)
5102{
5103 for (const std::string &type :
5104 caller_arg_types(func_name, param_index, search_context))
5105 {
5106 if (type.empty())
5107 return false;
5108 if (
5109 std::find(call_types.begin(), call_types.end(), type) == call_types.end())
5110 call_types.push_back(type);
5111 }
5112 return are_specializable_types(call_types);
5113}
5114
5115template <class Json>
5116std::string python_annotation<Json>::infer_parameter_type_from_calls(
5117 size_t param_index,
5118 const std::vector<Json> &function_calls)
5119{
5120 std::string inferred_type;
5121
5122 for (const Json &call : function_calls)
5123 {
5124 if (call.contains("args") && param_index < call["args"].size())
5125 {
5126 const Json &arg = call["args"][param_index];
5127 std::string arg_type = get_argument_type(arg);
5128
5129 if (!arg_type.empty())
5130 {
5131 if (inferred_type.empty())
5132 inferred_type = arg_type;
5133 else if (inferred_type != arg_type)
5134 {
5135 std::string common = find_common_ancestor(inferred_type, arg_type);
5136 // An empty literal (`[]`, `{}`) spells only the bare container,
5137 // which says nothing against the element type another call site
5138 // shows, so `list` and `list[int]` agree on `list[int]`. Without
5139 // this the parameter fell to "Any" and an `==` on it compared the
5140 // list pointers rather than the contents (#8302).
5141 const auto bare_of = [](const std::string &t) {
5142 const auto bracket = t.find('[');
5143 return bracket == std::string::npos ? t : t.substr(0, bracket);
5144 };
5145 const bool same_container =
5146 (inferred_type == "list" || inferred_type == "dict" ||
5147 inferred_type == "set" || arg_type == "list" ||
5148 arg_type == "dict" || arg_type == "set") &&
5149 bare_of(inferred_type) == bare_of(arg_type);
5150 if (same_container)
5151 {
5152 if (inferred_type.find('[') == std::string::npos)
5153 inferred_type = arg_type;
5154 }
5155 else if (!common.empty())
5156 inferred_type = common;
5157 // Tuples of differing shape have no common spelling, but "Any" is
5158 // the wrong retreat: it is exactly the type refine_any_param_to_list
5159 // retypes to the list model, which reports a spurious IndexError on
5160 // a tuple. The bare spelling keeps the parameter out of that path.
5161 else if (
5162 inferred_type.rfind("tuple", 0) == 0 &&
5163 arg_type.rfind("tuple", 0) == 0)
5164 inferred_type = "tuple";
5165 else
5166 return "Any";
5167 }
5168 }
5169 }
5170 }
5171
5172 return inferred_type;
5173}
5174
5176template <class Json>
5177static bool
5178aliases_function(const Json &stmt, const std::string &name, const Json &body)
5179{
5180 if (stmt.value("_type", "") != "Assign" || !stmt.contains("targets"))
5181 return false;
5182 const auto &targets = stmt["targets"];
5183 if (targets.empty() || targets[0].value("id", "") != name)
5184 return false;
5185 if (!stmt.contains("value") || stmt["value"].value("_type", "") != "Name")
5186 return false;
5187 return !json_utils::try_find_function(body, stmt["value"].value("id", ""))
5188 .empty();
5189}
5190
5196template <class Json>
5197std::string python_annotation<Json>::infer_type_from_parameter_default(
5198 const Json &function_element,
5199 size_t param_index,
5200 size_t param_count)
5201{
5202 const auto &args_node = function_element["args"];
5203 if (!args_node.contains("defaults"))
5204 return "";
5205
5206 const auto &defaults = args_node["defaults"];
5207 const size_t defaults_start = param_count - defaults.size();
5208 if (param_index < defaults_start)
5209 return "";
5210
5211 const Json &def_node = defaults[param_index - defaults_start];
5212 const std::string def_type = def_node.value("_type", "");
5213
5214 // Only a list literal is mapped: a bare `dict`/`set` annotation does not
5215 // reach the container model here (len() still lowers to strlen), and a
5216 // `tuple` one aborts the converter, so those stay untyped as before.
5217 if (def_type == "List")
5218 return "list";
5219
5220 if (def_type != "Name" || !def_node.contains("id"))
5221 return "";
5222
5223 const std::string def_name = def_node.value("id", "");
5224 const nlohmann::json &const_body = ast_["body"];
5225 if (!json_utils::try_find_function(const_body, def_name).empty())
5226 return "Any";
5227
5228 for (const auto &stmt : ast_["body"])
5229 if (aliases_function(stmt, def_name, const_body))
5230 return "Any";
5231 return "";
5232}
5233
5234template <class Json>
5235void python_annotation<Json>::infer_parameter_types(Json &function_element)
5236{
5237 const std::string &func_name = function_element["name"];
5238
5239 // Check if function has a meaningful body
5240 bool has_meaningful_body = false;
5241 if (function_element.contains("body") && !function_element["body"].empty())
5242 {
5243 for (const auto &stmt : function_element["body"])
5244 {
5245 if (stmt.contains("_type") && stmt["_type"] != "Pass")
5246 {
5247 has_meaningful_body = true;
5248 break;
5249 }
5250 }
5251 }
5252
5253 // Determine where to search for function calls:
5254 // - For nested functions: search in parent function's body
5255 // - For top-level functions: search in global AST body
5256 const Json &search_context =
5257 (parent_func != nullptr)
5258 ? (*parent_func)["body"] // Search in parent function's body
5259 : ast_["body"]; // Search in global body
5260
5261 // Find all calls to this function in the AST
5262 std::vector<Json> function_calls =
5263 find_function_calls(func_name, search_context);
5264
5265 // Determine the call-site context for resolving argument names.
5266 // For top-level functions, use global scope (empty context).
5267 // For nested functions, use the parent function context.
5268 std::string call_site_context;
5269 if (parent_func != nullptr)
5270 {
5271 std::string cur = current_func_name_context_;
5272 size_t pos = cur.rfind("@F@");
5273 if (pos != std::string::npos)
5274 call_site_context = cur.substr(0, pos);
5275 else if (parent_func->contains("name"))
5276 call_site_context = (*parent_func)["name"].template get<std::string>();
5277 }
5278
5279 // For each parameter, try to infer its type from the function calls
5280 if (
5281 function_element.contains("args") &&
5282 function_element["args"].contains("args"))
5283 {
5284 Json &params = function_element["args"]["args"];
5285
5286 for (size_t i = 0; i < params.size(); ++i)
5287 {
5288 Json &param = params[i];
5289
5290 // Check if parameter needs type inference or refinement
5291 bool needs_inference = false;
5292 bool has_partial_annotation = false;
5293 std::string current_type;
5294
5295 if (param.contains("annotation") && !param["annotation"].is_null())
5296 {
5297 // Extract current annotation
5298 if (param["annotation"].contains("id"))
5299 current_type = param["annotation"]["id"];
5300 // Check if it's a generic container without element type
5301 if (
5302 current_type == "list" || current_type == "dict" ||
5303 current_type == "set")
5304 {
5305 has_partial_annotation = true;
5306 needs_inference = true; // Try to refine with element type
5307 }
5308 else if (current_type == "tuple")
5309 {
5310 // Bare `tuple` annotation: try to specialise to
5311 // `tuple[T0, T1, ...]` by inspecting call sites so the unpacker
5312 // can recover the struct shape (GitHub #4515).
5313 if (!function_calls.empty())
5314 {
5315 std::vector<std::string> elem_types =
5316 infer_tuple_param_types_from_calls(i, function_calls);
5317 if (!elem_types.empty())
5318 {
5319 int lineno = param.value("lineno", 0);
5320 int col_offset = param.value("col_offset", 0) +
5321 param["arg"].template get<std::string>().size() +
5322 1;
5323 param["annotation"] = create_tuple_subscript_annotation(
5324 elem_types, lineno, col_offset, lineno);
5325 }
5326 }
5327 continue;
5328 }
5329 else
5330 continue; // Fully annotated, skip
5331 }
5332 else
5333 needs_inference = true; // No annotation at all
5334
5335 std::string inferred_type;
5336
5337 // Leave the parameter unannotated if a call site feeds it a variable
5338 // that's genuinely dynamically-typed (e.g. `x = 1`/`x = "a"` across an
5339 // if/else), so it falls through to register_function_argument's own
5340 // tagged-scalar detection instead of locking in a wrong concrete type.
5341 bool arg_is_dynamically_typed = false;
5342 if (needs_inference)
5343 {
5344 for (const Json &call : function_calls)
5345 {
5346 if (!call.contains("args") || i >= call["args"].size())
5347 continue;
5348 const Json &arg_node = call["args"][i];
5349 if (
5350 arg_node.contains("_type") && arg_node["_type"] == "Name" &&
5351 arg_node.contains("id") &&
5353 arg_node["id"].template get<std::string>(), search_context))
5354 {
5355 arg_is_dynamically_typed = true;
5356 break;
5357 }
5358 }
5359 }
5360
5361 // Try to infer type from function calls if available
5362 if (
5363 needs_inference && !arg_is_dynamically_typed && !function_calls.empty())
5364 {
5365 std::vector<std::string> call_types =
5366 collect_parameter_types_from_calls(i, function_calls);
5367
5368 if (
5369 parent_func == nullptr && call_types.size() > 1 &&
5370 specializable_at_call_sites(func_name, i, call_types, search_context))
5371 {
5372 if (!is_specialization_pending(func_name, i, call_types))
5373 {
5374 pending_specializations_.push_back(
5375 {func_name, i, std::move(call_types)});
5376 }
5377
5378 continue;
5379 }
5380
5381 std::string saved_ctx = current_func_name_context_;
5382 current_func_name_context_ = call_site_context;
5383 inferred_type = infer_parameter_type_at_call_sites(
5384 func_name, i, function_calls, search_context);
5385 current_func_name_context_ = saved_ctx;
5386 }
5387
5388 // If still no type, try to infer from the parameter's default value.
5389 if (inferred_type.empty() && !has_partial_annotation)
5390 inferred_type =
5391 infer_type_from_parameter_default(function_element, i, params.size());
5392
5393 // For __getitem__/__setitem__ key parameters, infer a
5394 // `tuple[slice, slice, ...]` annotation from multi-axis subscript
5395 // call sites on instances of the enclosing class (GitHub #4539).
5396 // The Subscript syntax t[i:j, k:l] never appears as an explicit
5397 // __getitem__ call in the AST, so the standard call-site inference
5398 // never sees it. Without this, `key` stays unannotated and `key[0]`
5399 // inside __getitem__ falls through to list indexing.
5400 if (
5401 inferred_type.empty() && !current_class_name_.empty() &&
5402 (func_name == "__getitem__" || func_name == "__setitem__") && i == 1)
5403 {
5404 std::vector<std::string> elem_types =
5405 infer_subscript_key_tuple_types(current_class_name_);
5406 if (!elem_types.empty())
5407 {
5408 int lineno = param.value("lineno", 0);
5409 int col_offset = param.value("col_offset", 0) +
5410 param["arg"].template get<std::string>().size() + 1;
5411 param["annotation"] = create_tuple_subscript_annotation(
5412 elem_types, lineno, col_offset, lineno);
5413 continue;
5414 }
5415 }
5416
5417 // Apply inference results
5418 if (has_partial_annotation)
5419 {
5420 // Refine partial annotation with inferred element type
5421 if (!inferred_type.empty() && inferred_type != current_type)
5422 add_parameter_annotation(param, inferred_type);
5423 }
5424 else
5425 {
5426 // Handle completely unannotated parameters
5427 if (inferred_type.empty() && !has_meaningful_body)
5428 inferred_type = "Any"; // Default fallback for stub functions
5429
5430 if (!inferred_type.empty())
5431 {
5432 // Add annotation to parameter
5433 add_parameter_annotation(param, inferred_type);
5434 }
5435 }
5436 }
5437 }
5438}
5439
5440// ---------- preprocessing and orchestration ----------
5441
5444template <class Json>
5445Json *python_annotation<Json>::class_init(
5446 const std::string &class_name,
5447 unsigned depth,
5448 bool &inherited)
5449{
5450 if (depth > 32)
5451 return nullptr;
5452 for (Json &class_node : ast_["body"])
5453 {
5454 if (class_node["_type"] != "ClassDef" || class_node["name"] != class_name)
5455 continue;
5456 for (Json &member : class_node["body"])
5457 if (member["_type"] == "FunctionDef" && member["name"] == "__init__")
5458 {
5459 inherited = depth > 0;
5460 return &member;
5461 }
5462 if (class_node.contains("bases"))
5463 for (const Json &base : class_node["bases"])
5464 if (base.value("_type", "") == "Name")
5465 if (Json *init = class_init(base["id"], depth + 1, inherited))
5466 return init;
5467 return nullptr;
5468 }
5469 return nullptr;
5470}
5471
5474template <class Json>
5475static bool is_dynamically_typed_name(const Json &arg, const Json &scope)
5476{
5477 return arg.value("_type", "") == "Name" &&
5479 arg["id"].template get<std::string>(), scope);
5480}
5481
5482template <class Json>
5484{
5485 bool annotated = false;
5486 if (node.is_object())
5487 {
5488 // Check if this is a constructor call
5489 if (
5490 node.contains("_type") && node["_type"] == "Call" &&
5491 node.contains("func") && node["func"]["_type"] == "Name")
5492 {
5493 bool inherited = false;
5494 Json *init = class_init(node["func"]["id"], 0, inherited);
5495 if (init && init->contains("args") && (*init)["args"].contains("args"))
5496 {
5497 Json &params = (*init)["args"]["args"];
5498 const Json &call_args = node["args"];
5499 // Skip self (index 0), match remaining params with call args
5500 for (size_t i = 1; i < params.size() && (i - 1) < call_args.size(); ++i)
5501 {
5502 Json &param = params[i];
5503 // Only annotate if the parameter is not yet annotated;
5504 // do not overwrite explicit annotations (e.g.,
5505 // Optional["List"]) with a less-specific type inferred from a
5506 // call site (e.g., NoneType).
5507 if (param.contains("annotation") && !param["annotation"].is_null())
5508 continue;
5509 const Json &arg = call_args[i - 1];
5510 const Json &scope =
5511 current_func != nullptr ? (*current_func)["body"] : ast_["body"];
5512 if (is_dynamically_typed_name(arg, scope))
5513 continue;
5514 std::string arg_type = get_argument_type(arg);
5515 // Subclasses calling one inherited __init__ with different types
5516 // leave the parameter to the runtime type (type_eq_erased_type).
5517 if (inherited)
5518 inherited_init_args_[&param].insert(arg_type);
5519 else if (!arg_type.empty())
5520 {
5521 add_parameter_annotation(param, arg_type);
5522 annotated = true;
5523 }
5524 }
5525 }
5526 }
5527 // A method's parameters are what its constructor calls' arguments name.
5528 const bool is_function_def =
5529 node.contains("_type") && node["_type"] == "FunctionDef" &&
5530 node.contains("name") && node["name"].is_string();
5531 const bool is_class_def =
5532 node.contains("_type") && node["_type"] == "ClassDef" &&
5533 node.contains("name") && node["name"].is_string();
5534 Json *saved_current = current_func;
5535 Json *saved_parent = parent_func;
5536 const std::string saved_ctx = current_func_name_context_;
5537 const std::string saved_class = current_class_name_;
5538 if (is_class_def)
5539 current_class_name_ = node["name"].template get<std::string>();
5540 if (is_function_def)
5541 {
5542 const std::string name = node["name"].template get<std::string>();
5543 current_func_name_context_ =
5544 !saved_ctx.empty() ? saved_ctx + "@F@" + name
5545 : !current_class_name_.empty() ? current_class_name_ + "@C@" + name
5546 : name;
5547 parent_func = saved_current;
5548 current_func = &node;
5549 }
5550 // Recursively search all object fields
5551 for (auto &kv : node.items())
5552 annotated |= preprocess_constructor_calls(kv.value());
5553 current_class_name_ = saved_class;
5554 if (is_function_def)
5555 {
5556 current_func_name_context_ = saved_ctx;
5557 current_func = saved_current;
5558 parent_func = saved_parent;
5559 }
5560 }
5561 else if (node.is_array())
5562 {
5563 // Search in arrays (such as list literals)
5564 for (auto &element : node)
5565 annotated |= preprocess_constructor_calls(element);
5566 }
5567 return annotated;
5568}
5569
5570template <class Json>
5572{
5573 if (node.is_object())
5574 {
5575 // Detect: A().method(args)
5576 // func._type == "Attribute", func.value._type == "Call" (the A() part),
5577 // func.value.func._type == "Name" (the class name A)
5578 if (
5579 node.contains("_type") && node["_type"] == "Call" &&
5580 node.contains("func") && node["func"]["_type"] == "Attribute" &&
5581 node["func"].contains("value") &&
5582 node["func"]["value"]["_type"] == "Call" &&
5583 node["func"]["value"].contains("func") &&
5584 node["func"]["value"]["func"]["_type"] == "Name" &&
5585 node["func"]["value"]["func"].contains("id") &&
5586 node["func"].contains("attr"))
5587 {
5588 const std::string &class_name =
5589 node["func"]["value"]["func"]["id"].template get<std::string>();
5590 const std::string &method_name =
5591 node["func"]["attr"].template get<std::string>();
5592 const Json &call_args =
5593 node.contains("args") ? node["args"] : Json::array();
5594
5595 for (Json &class_node : ast_["body"])
5596 {
5597 if (
5598 class_node["_type"] == "ClassDef" && class_node["name"] == class_name)
5599 {
5600 for (Json &member : class_node["body"])
5601 {
5602 if (
5603 member["_type"] == "FunctionDef" &&
5604 member["name"] == method_name && member.contains("args") &&
5605 member["args"].contains("args"))
5606 {
5607 Json &params = member["args"]["args"];
5608 // Skip self (index 0); match remaining params to call args
5609 for (size_t i = 1;
5610 i < params.size() && (i - 1) < call_args.size();
5611 ++i)
5612 {
5613 Json &param = params[i];
5614 // Only annotate if the parameter is not yet annotated
5615 if (
5616 param.contains("annotation") &&
5617 !param["annotation"].is_null())
5618 continue;
5619 const Json &arg = call_args[i - 1];
5620 std::string arg_type = get_argument_type(arg);
5621 if (!arg_type.empty())
5622 add_parameter_annotation(param, arg_type);
5623 }
5624 }
5625 }
5626 }
5627 }
5628 }
5629
5630 // Recursively search all object fields
5631 for (auto &kv : node.items())
5632 preprocess_method_calls(kv.value());
5633 }
5634 else if (node.is_array())
5635 {
5636 for (const auto &element : node)
5637 preprocess_method_calls(element);
5638 }
5639}
5640
5642template <class Json>
5643Json *
5644python_annotation<Json>::class_method(const std::string &cls, const Json &method)
5645{
5646 for (Json &class_node : ast_["body"])
5647 if (class_node["_type"] == "ClassDef" && class_node["name"] == cls)
5648 for (Json &member : class_node["body"])
5649 if (member["_type"] == "FunctionDef" && member["name"] == method)
5650 return &member;
5651 return nullptr;
5652}
5653
5656template <class Json>
5658{
5659 if (func.value("_type", "") != "Name")
5660 return false;
5661 const std::string &name = func["id"].template get<std::string>();
5662 return !json_utils::is_class<Json>(name, ast_) &&
5663 json_utils::try_find_function(ast_["body"], name).is_null();
5664}
5665
5670template <class Json>
5672{
5673 std::map<std::pair<Json *, size_t>, std::set<std::string>> seen;
5674 std::function<void(const Json &)> walk = [&](const Json &node) {
5675 if (node.is_array())
5676 {
5677 for (const Json &item : node)
5678 walk(item);
5679 return;
5680 }
5681 if (!node.is_object())
5682 return;
5683 const std::string kind = node.value("_type", "");
5684 if (kind == "FunctionDef" || kind == "ClassDef" || kind == "Lambda")
5685 return;
5686 const Json &func = node.value("func", Json());
5687 // `obj(args)` on a callable instance calls `obj.__call__(args)` (#9070).
5688 const bool instance_call = kind == "Call" && is_instance_call(func);
5689 const bool method_call =
5690 kind == "Call" && func.value("_type", "") == "Attribute";
5691 const Json receiver =
5692 instance_call ? func : method_call ? func["value"] : Json();
5693 if (
5694 (instance_call || method_call) &&
5695 receiver.value("_type", "") == "Name" &&
5696 !is_rebound_in_scope(receiver["id"].template get<std::string>()))
5697 {
5698 const std::string attr =
5699 instance_call ? "__call__" : func["attr"].template get<std::string>();
5700 for (const std::string &cls :
5701 get_class_ancestors(get_argument_type(receiver)))
5702 if (Json *method = class_method(cls, attr))
5703 {
5704 // A static or class method has no `self` to skip.
5705 if (!(*method)["decorator_list"].empty())
5706 break;
5707 const Json &params = (*method)["args"]["args"];
5708 for (size_t i = 1; i < params.size(); ++i)
5709 seen[{method, i}].insert(
5710 i - 1 < node["args"].size() &&
5711 !is_dynamically_typed_name(node["args"][i - 1], ast_["body"])
5712 ? get_argument_type(node["args"][i - 1])
5713 : std::string());
5714 break;
5715 }
5716 }
5717 for (const auto &kv : node.items())
5718 if (kv.value().is_structured())
5719 walk(kv.value());
5720 };
5721 walk(ast_["body"]);
5722
5723 for (const auto &[key, types] : seen)
5724 {
5725 Json &param = (*key.first)["args"]["args"][key.second];
5726 if (
5727 types.size() == 1 && !types.begin()->empty() &&
5728 param.value("annotation", Json()).is_null())
5729 add_parameter_annotation(param, *types.begin());
5730 }
5731}
5732
5752template <class Json>
5754{
5755 // First pass: gather inferred argument types for every top-level
5756 // function call. Recurse into FunctionDef bodies so scope-dependent
5757 // type inference resolves correctly. Self-recursive calls are
5758 // skipped so the function's own internal use of its parameter does
5759 // not poison the inference.
5760 std::set<std::string> top_level_funcs;
5761 for (const Json &top_node : ast_["body"])
5762 {
5763 if (
5764 top_node["_type"] == "FunctionDef" && top_node.contains("name") &&
5765 top_node["name"].is_string())
5766 top_level_funcs.insert(top_node["name"].template get<std::string>());
5767 }
5768
5769 std::map<std::pair<std::string, size_t>, std::set<std::string>> param_types;
5770 std::set<std::pair<std::string, size_t>> dynamically_typed_params;
5772 root, param_types, top_level_funcs, dynamically_typed_params);
5773
5774 // Second pass: for each top-level FunctionDef, if all observed call
5775 // sites agreed on a single type for a parameter, write the annotation.
5776 bool annotated = false;
5777 for (Json &top_node : ast_["body"])
5778 {
5779 if (
5780 top_node["_type"] != "FunctionDef" || !top_node.contains("args") ||
5781 !top_node["args"].contains("args"))
5782 continue;
5783
5784 const std::string fname = top_node["name"].template get<std::string>();
5785 Json &params = top_node["args"]["args"];
5786 for (size_t i = 0; i < params.size(); ++i)
5787 {
5788 Json &param = params[i];
5789 if (param.contains("annotation") && !param["annotation"].is_null())
5790 continue;
5791 // Leave it unannotated even if another call site agrees on one
5792 // concrete type -- see the dynamically-typed check above.
5793 if (dynamically_typed_params.count({fname, i}) != 0)
5794 continue;
5795 auto it = param_types.find({fname, i});
5796 if (it != param_types.end())
5797 annotated |= annotate_from_call_sites(
5798 root, param, {fname, i}, it->second, top_level_funcs);
5799 }
5800 }
5801 return annotated;
5802}
5803
5809template <class Json>
5811 Json &root,
5812 Json &param,
5813 const std::pair<std::string, size_t> &key,
5814 std::set<std::string> types,
5815 const std::set<std::string> &top_level_funcs)
5816{
5817 const bool has_unresolved = types.erase(unresolved_arg_type()) != 0;
5818 if (types.size() != 1)
5819 return false;
5820 const Json unannotated = param;
5821 add_parameter_annotation(param, *types.begin());
5822 if (!has_unresolved)
5823 return true;
5824
5825 std::map<std::pair<std::string, size_t>, std::set<std::string>> rechecked;
5826 std::set<std::pair<std::string, size_t>> dynamically_typed;
5828 root, rechecked, top_level_funcs, dynamically_typed);
5829 if (rechecked[key] == types)
5830 return true;
5831 param = unannotated;
5832 return false;
5833}
5834
5835template <class Json>
5837 Json &node,
5838 std::map<std::pair<std::string, size_t>, std::set<std::string>> &param_types,
5839 const std::set<std::string> &top_level_funcs,
5840 std::set<std::pair<std::string, size_t>> &dynamically_typed_params)
5841{
5842 if (node.is_object())
5843 {
5844 if (
5845 node.contains("_type") && node["_type"] == "Call" &&
5846 node.contains("func") && node["func"].is_object() &&
5847 node["func"].contains("_type") && node["func"]["_type"] == "Name" &&
5848 node["func"].contains("id") && node["func"]["id"].is_string())
5849 {
5850 const std::string func_name =
5851 node["func"]["id"].template get<std::string>();
5852 // Only record calls targeting a top-level FunctionDef (the only
5853 // shape this pass annotates). Filtering up front also keeps
5854 // nested-def shadows from polluting the per-name type set. Skip
5855 // self-recursive calls so the function's own internal use of its
5856 // parameter does not poison inference.
5857 if (
5858 top_level_funcs.count(func_name) != 0 &&
5859 func_name != current_func_name_context_)
5860 {
5861 const Json &call_args =
5862 node.contains("args") ? node["args"] : Json::array();
5863 const Json &enclosing_scope_body =
5864 current_func != nullptr ? (*current_func)["body"] : ast_["body"];
5865 for (size_t i = 0; i < call_args.size(); ++i)
5866 {
5867 const Json &arg_node = call_args[i];
5868 // Same dynamically-typed check as above, applied to this pass's
5869 // own call-site scan.
5870 if (
5871 arg_node.contains("_type") && arg_node["_type"] == "Name" &&
5872 arg_node.contains("id") &&
5874 arg_node["id"].template get<std::string>(), enclosing_scope_body))
5875 {
5876 dynamically_typed_params.insert({func_name, i});
5877 continue;
5878 }
5879
5880 std::string arg_type = get_argument_type(arg_node);
5881 // Ambiguous / under-specified types: NoneType is `bool*` in the
5882 // operational model (issue #3796) and Any leaves the type
5883 // uninformative; both would lock in a misleading annotation for
5884 // callers that pass a real value. Recorded rather than dropped so
5885 // a parameter is annotated only once *every* call site resolves —
5886 // an iterating pass must not commit to a type a later round
5887 // contradicts (GitHub #7254).
5888 if (is_unresolved_arg_type(arg_type))
5889 arg_type = unresolved_arg_type();
5890 param_types[{func_name, i}].insert(arg_type);
5891 }
5892 }
5893 }
5894
5895 const bool is_function_def =
5896 node.contains("_type") && node["_type"] == "FunctionDef" &&
5897 node.contains("name") && node["name"].is_string();
5898 std::string saved_ctx = current_func_name_context_;
5899 Json *saved_current = current_func;
5900 Json *saved_parent = parent_func;
5901 if (is_function_def)
5902 {
5903 // Set up parent/current scope so get_argument_type's nested-scope
5904 // fallback (parent_func body lookup) can resolve closure variables
5905 // captured by nested function bodies.
5906 current_func_name_context_ = node["name"].template get<std::string>();
5907 parent_func = saved_current;
5908 current_func = &node;
5909 }
5910
5911 for (auto &kv : node.items())
5913 kv.value(), param_types, top_level_funcs, dynamically_typed_params);
5914
5915 if (is_function_def)
5916 {
5917 current_func_name_context_ = saved_ctx;
5918 current_func = saved_current;
5919 parent_func = saved_parent;
5920 }
5921 }
5922 else if (node.is_array())
5923 {
5924 for (auto &element : node)
5926 element, param_types, top_level_funcs, dynamically_typed_params);
5927 }
5928}
5929
5930template <class Json>
5932{
5933 // Iterated so a constructor called with its caller's parameter sees the
5934 // type that parameter gets from a later call (GitHub #8963).
5935 while (preprocess_constructor_calls(ast_))
5936 ;
5937 for (const auto &[param, types] : inherited_init_args_)
5938 if (
5939 types.size() == 1 && !types.begin()->empty() &&
5940 param->value("annotation", Json()).is_null())
5941 add_parameter_annotation(*param, *types.begin());
5942 inherited_init_args_.clear();
5943 // Also preprocess method calls on temporary instances: A().method(args)
5946 // Preprocess top-level function calls f(arg) so unannotated parameters of
5947 // top-level functions inherit a type from their callers. Runs after the
5948 // constructor pass so `f(SharedResource())` resolves through the
5949 // inferred return type. Without this, attribute-chain lookups on
5950 // unannotated parameters (e.g. `resource.mutex.acquire()`) fail with a
5951 // cryptic "Variable mutex not found" — GitHub #4570.
5952 // Iterated: each round resolves the arguments the previous round annotated,
5953 // carrying types one call-graph edge further — a callee whose caller's own
5954 // parameters this pass types stays Any after a single round (GitHub #7254).
5955 // Terminates because a round returns true only after annotating a parameter
5956 // that had none, and annotated parameters are never revisited.
5958 ;
5959
5960 // Second pass: add type annotations to global scope variables
5961 annotate_global_scope();
5962 current_line_ = 0;
5963
5964 // Add type annotation to all functions and class methods
5965 for (Json &element : ast_["body"])
5966 {
5967 // Process top-level functions
5968 if (element["_type"] == "FunctionDef")
5969 annotate_function(element);
5970 // Process classes and their methods
5971 else if (element["_type"] == "ClassDef")
5972 annotate_class(element);
5973 }
5974
5975 apply_pending_specializations();
5976}
5977
5978template <class Json>
5980 const Json &ast)
5981{
5982 extra_subscript_inference_asts_.push_back(&ast);
5983}
5984
5985template <class Json>
5986void python_annotation<Json>::add_type_annotation(const std::string &func_name)
5987{
5988 current_line_ = 0;
5989 annotating_function_entry_point_ = true;
5990
5991 for (Json &elem : ast_["body"])
5992 {
5993 if (elem["_type"] == "FunctionDef" && elem["name"] == func_name)
5994 {
5995 get_global_elements(elem["body"]);
5996 // Add type annotations to global scope variables
5997 if (!referenced_global_elements.empty())
5998 annotate_global_scope();
5999 filter_global_elements_ = false;
6000
6001 // Add annotation to a specific function
6002 annotate_function(elem);
6003 return;
6004 }
6005 }
6006}
6007
6008template <class Json>
6009void python_annotation<Json>::get_global_elements(const Json &node)
6010{
6011 // Checks if the current node is a variable identifier
6012 if (node.contains("_type") && node["_type"] == "Name" && node.contains("id"))
6013 {
6014 const std::string &var_name = node["id"];
6015 Json var_node = json_utils::find_var_decl(var_name, "", ast_);
6016 if (!var_node.empty())
6017 {
6018 gs_.add_variable(var_name);
6019 referenced_global_elements.push_back(var_node);
6020 }
6021 }
6022
6023 if (
6024 node.contains("_type") && node["_type"] == "Call" &&
6025 node.contains("func") && node["func"]["_type"] == "Name")
6026 {
6027 const std::string &class_name = node["func"]["id"];
6028 // Checks if the current node is a constructor call
6029 Json class_node = json_utils::find_class(ast_["body"], class_name);
6030 if (!class_node.empty())
6031 {
6032 gs_.add_class(class_name);
6033 referenced_global_elements.push_back(class_node);
6034 }
6035 else
6036 {
6037 const auto &func_name = node["func"]["id"];
6038 if (!type_utils::is_builtin_type(func_name))
6039 {
6040 try
6041 {
6042 auto func_node =
6043 json_utils::find_function_or_throw(ast_["body"], func_name);
6044 get_global_elements(func_node);
6045 }
6046 catch (std::runtime_error &)
6047 {
6048 }
6049 }
6050 }
6051 }
6052
6053 // Recursively iterates through all fields of the node if it is an object
6054 if (node.is_object())
6055 {
6056 for (auto it = node.begin(); it != node.end(); ++it)
6057 get_global_elements(it.value());
6058 }
6059
6060 // Iterates over the elements if the current node is an array
6061 else if (node.is_array())
6062 {
6063 for (const auto &element : node)
6064 get_global_elements(element);
6065 }
6066 filter_global_elements_ = true;
6067}
6068
6069template <class Json>
6070void python_annotation<Json>::annotate_global_scope()
6071{
6072 add_annotation(ast_);
6073}
6074
6075template <class Json>
6076void python_annotation<Json>::annotate_function(Json &function_element)
6077{
6078 std::string saved_func_name_context = current_func_name_context_;
6079 Json *saved_parent_func = parent_func; // Save previous parent
6080
6081 const std::string &func_name =
6082 function_element["name"].template get<std::string>();
6083
6084 if (!saved_func_name_context.empty())
6085 {
6086 // Nested function, including one inside a method - accumulate context
6087 current_func_name_context_ = saved_func_name_context + "@F@" + func_name;
6088 }
6089 else if (!current_class_name_.empty())
6090 {
6091 // Method: scope it by its class so same-named methods stay distinct
6092 current_func_name_context_ = current_class_name_ + "@C@" + func_name;
6093 }
6094 else
6095 {
6096 // Top-level function
6097 current_func_name_context_ = func_name;
6098 }
6099
6100 parent_func = current_func; // Current becomes parent
6101 current_func = &function_element;
6102
6103 // Infer types for unannotated parameters based on function calls
6104 infer_parameter_types(function_element);
6105
6106 // Add type annotations within the function
6107 add_annotation(function_element);
6108
6109 // Skip return type inference for __init__ (constructors always return None)
6110 if (func_name == "__init__")
6111 {
6112 // Constructors return None by default - no need to infer
6113 if (function_element["returns"].is_null())
6114 {
6115 function_element["returns"] = {
6116 {"_type", "Constant"},
6117 {"value", nullptr}, // None
6118 {"lineno", function_element["lineno"]},
6119 {"col_offset", function_element["col_offset"]},
6120 {"end_lineno", function_element["lineno"]},
6121 {"end_col_offset",
6122 function_element["col_offset"].template get<int>() + 4}};
6123 }
6124
6125 // Update the end column offset after adding annotations
6126 update_end_col_offset(function_element);
6127 current_func = nullptr;
6128 parent_func = saved_parent_func; // Restore previous parent
6129 current_func_name_context_ = saved_func_name_context;
6130 return; // Exit early for __init__
6131 }
6132
6133 // Check if we should override the return annotation
6134 bool should_override =
6135 config.options.get_bool_option("override-return-annotation");
6136 bool has_no_annotation = function_element["returns"].is_null();
6137
6138 if (has_no_annotation || should_override)
6139 {
6140 std::string inferred_type =
6141 infer_from_return_statements(function_element["body"], func_name);
6142
6143 // Only add annotation if we successfully inferred a type from return
6144 // statements and the function does not have mixed value+None returns
6145 if (!inferred_type.empty() && inferred_type != "NoneType")
6146 {
6147 // Check if there are also None returns (mixed value+None)
6148 // If so, leave the annotation as null for the converter to handle
6149 // via Optional wrapping
6150 bool has_none_return = has_return_none(function_element["body"]);
6151
6152 if (!has_none_return)
6153 {
6154 // Update the function node to include the return type annotation.
6155 // Marked _inferred_annotation so a downstream converter check (e.g.
6156 // local numpy array returns) can tell this apart from an annotation
6157 // the user actually wrote -- should_override replaces an explicit
6158 // one only because the user opted into that via
6159 // override-return-annotation, so it is not authoritative either.
6160 function_element["returns"] = {
6161 {"_type", "Name"},
6162 {"id", inferred_type},
6163 {"ctx", {{"_type", "Load"}}},
6164 {"lineno", function_element["lineno"]},
6165 {"col_offset", function_element["col_offset"]},
6166 {"end_lineno", function_element["lineno"]},
6167 {"end_col_offset",
6168 function_element["col_offset"].template get<int>() +
6169 inferred_type.size()},
6170 {"_inferred_annotation", true}};
6171 }
6172 }
6173 else if (inferred_type == "NoneType")
6174 {
6175 // Function only returns None - annotate as None
6176 function_element["returns"] = {
6177 {"_type", "Constant"},
6178 {"value", nullptr},
6179 {"lineno", function_element["lineno"]},
6180 {"col_offset", function_element["col_offset"]},
6181 {"end_lineno", function_element["lineno"]},
6182 {"end_col_offset",
6183 function_element["col_offset"].template get<int>() + 4},
6184 {"_inferred_annotation", true}};
6185 }
6186 // If no return type could be inferred, leave returns as null
6187 // (function has no explicit return statement)
6188 }
6189
6190 // Update the end column offset after adding annotations
6191 update_end_col_offset(function_element);
6192
6193 current_func = nullptr;
6194 parent_func = saved_parent_func; // Restore previous parent
6195 current_func_name_context_ = saved_func_name_context;
6196}
6197
6198template <class Json>
6199void python_annotation<Json>::annotate_class(Json &class_element)
6200{
6201 std::string saved_class_name = current_class_name_;
6202 std::string saved_context = current_func_name_context_;
6203
6204 current_class_name_ = class_element["name"].template get<std::string>();
6205 current_func_name_context_.clear();
6206
6207 for (Json &class_member : class_element["body"])
6208 {
6209 // Process methods in the class
6210 if (class_member["_type"] == "FunctionDef")
6211 {
6212 // Add type annotations within the class member function
6213 annotate_function(class_member);
6214 }
6215 // Process unannotated class attributes (e.g., species = "Homo sapiens")
6216 else if (class_member["_type"] == "Assign")
6217 {
6218 std::string inferred_type;
6219
6220 // Infer type from the RHS value
6221 if (
6222 infer_type(class_member, class_element, inferred_type) ==
6223 InferResult::OK)
6224 {
6225 // Convert Assign to AnnAssign with the inferred type
6226 update_assignment_node(class_member, inferred_type);
6227 }
6228 else
6229 {
6230 // If type inference fails, throw error with helpful message
6231 std::string attr_name =
6232 class_member["targets"][0]["id"].template get<std::string>();
6233 throw std::runtime_error(
6234 "Cannot infer type for class attribute '" + attr_name +
6235 "' in class '" + class_element["name"].template get<std::string>() +
6236 "'. Please add explicit type annotation.");
6237 }
6238 }
6239 }
6240
6241 current_class_name_ = saved_class_name;
6242 current_func_name_context_ = saved_context;
6243}
6244
6245template <class Json>
6246void python_annotation<Json>::infer_loop_variable_types(
6247 Json &while_stmt,
6248 bool synthesized_only)
6249{
6250 if (!while_stmt.contains("body"))
6251 return;
6252
6253 // The tuple the lowered loop variable holds, and its name. Shared by every
6254 // lowered loop in a function, so its unpacking reads are typed here rather
6255 // than through a function-wide declaration lookup.
6256 std::string loop_var, loop_var_type;
6257 for (auto &stmt : while_stmt["body"])
6258 {
6259 // `a = loop_var[i]`, which the preprocessor emits to unpack a tuple target
6260 if (
6261 !loop_var.empty() && stmt["_type"] == "Assign" &&
6262 stmt["targets"].size() == 1 && stmt["targets"][0]["_type"] == "Name" &&
6263 stmt["value"]["_type"] == "Subscript" &&
6264 stmt["value"]["value"].value("id", "") == loop_var &&
6265 stmt["value"]["slice"].value("_type", "") == "Constant" &&
6266 stmt["value"]["slice"]["value"].is_number_unsigned())
6267 {
6268 const std::string type = unpacked_name_element_type(
6269 loop_var_type, stmt["value"]["slice"]["value"].template get<size_t>());
6270 if (type != "Any")
6271 update_assignment_node(stmt, type);
6272 continue;
6273 }
6274
6275 // Look for pattern: loop_var: Any = iterable[ESBMC_index_N]
6276 // A bare annotation like `x: int` has value == null; nlohmann::json's
6277 // `contains("value")` returns true for present-but-null members, so an
6278 // explicit is_null() guard is required to avoid a type_error on the
6279 // subsequent subscript.
6280 if (
6281 stmt["_type"] == "AnnAssign" && stmt.contains("value") &&
6282 !stmt["value"].is_null() && stmt["value"]["_type"] == "Subscript")
6283 {
6284 if (
6285 !stmt["value"]["value"].contains("id") ||
6286 (synthesized_only && !stmt.value("esbmc_synthesized", false)))
6287 continue;
6288
6289 std::string iter_var =
6290 stmt["value"]["value"]["id"].template get<std::string>();
6291
6292 // Find the iterable's annotation
6293 Json iter_node;
6294 if (current_func != nullptr && (*current_func).contains("body"))
6295 iter_node = find_annotated_assign(iter_var, (*current_func)["body"]);
6296 if (
6297 iter_node.empty() && current_func != nullptr &&
6298 (*current_func).contains("args"))
6299 iter_node =
6300 find_annotated_assign(iter_var, (*current_func)["args"]["args"]);
6301 if (iter_node.empty())
6302 iter_node = find_annotated_assign(iter_var, ast_["body"]);
6303
6304 if (iter_node.empty() || !iter_node.contains("annotation"))
6305 continue;
6306
6307 auto &iter_annotation = iter_node["annotation"];
6308
6309 // Extract element type from container annotation
6310 if (
6311 iter_annotation["_type"] == "Subscript" &&
6312 iter_annotation.contains("value") &&
6313 iter_annotation["value"].contains("id"))
6314 {
6315 std::string container_type =
6316 iter_annotation["value"]["id"].template get<std::string>();
6317
6318 if (container_type == "list" || container_type == "List")
6319 {
6320 // Extract T from list[T]
6321 if (iter_annotation.contains("slice"))
6322 stmt["annotation"] = list_element_annotation(iter_node);
6323 }
6324 else if (container_type == "dict" || container_type == "Dict")
6325 {
6326 // For dict iteration, iterate over keys (first type parameter)
6327 if (
6328 iter_annotation.contains("slice") &&
6329 iter_annotation["slice"]["_type"] == "Tuple" &&
6330 iter_annotation["slice"].contains("elts") &&
6331 !iter_annotation["slice"]["elts"].empty())
6332 {
6333 stmt["annotation"] = iter_annotation["slice"]["elts"][0];
6334 }
6335 }
6336 }
6337 else if (iter_annotation["_type"] == "Name")
6338 {
6339 // For str iteration, element type is also str
6340 std::string type_name =
6341 iter_annotation["id"].template get<std::string>();
6342 if (type_name == "str")
6343 stmt["annotation"] = iter_annotation;
6344 }
6345
6346 if (stmt.value("esbmc_synthesized", false))
6347 {
6348 loop_var = stmt["target"].value("id", "");
6349 loop_var_type = json_utils::render_annotation_type(stmt["annotation"]);
6350 }
6351 }
6352 }
6353}
6354
6357template <class Json>
6358Json python_annotation<Json>::list_element_annotation(const Json &iter_node)
6359{
6360 const Json &elem = iter_node["annotation"]["slice"];
6361 const Json &value = iter_node.value("value", Json());
6362 if (
6363 elem.value("id", "") != "Any" || !value.is_object() ||
6364 value.value("_type", "") != "Attribute" ||
6365 value["value"].value("id", "") != "self")
6366 return elem;
6367
6368 const Json attr_elem =
6369 self_attr_list_element(value["attr"].template get<std::string>());
6370 return attr_elem.is_null() ? elem : attr_elem;
6371}
6372
6376template <class Json>
6377Json python_annotation<Json>::self_attr_list_element(const std::string &attr)
6378{
6379 const Json cls = json_utils::find_class(ast_["body"], current_class_name_);
6380 if (cls.empty())
6381 return Json();
6382
6383 auto is_self_attr = [&attr](const Json &node) {
6384 return node.is_object() && node.value("_type", "") == "Attribute" &&
6385 node.value("attr", "") == attr && node.contains("value") &&
6386 node["value"].value("id", "") == "self";
6387 };
6388 size_t stores = 0;
6389 std::function<void(const Json &)> count = [&](const Json &node) {
6390 if (
6391 is_self_attr(node) && node.contains("ctx") &&
6392 node["ctx"].value("_type", "") == "Store")
6393 ++stores;
6394 if (node.is_structured())
6395 for (const Json &child : node)
6396 count(child);
6397 };
6398 count(cls);
6399 if (stores != 1)
6400 return Json();
6401
6402 const Json init = json_utils::try_find_function(cls["body"], "__init__");
6403 if (init.empty())
6404 return Json();
6405
6406 auto list_slice = [](const Json &ann) {
6407 return ann.is_object() && ann.value("_type", "") == "Subscript" &&
6408 ann["value"].value("id", "") == "list"
6409 ? ann["slice"]
6410 : Json();
6411 };
6412 for (const Json &stmt : init["body"])
6413 {
6414 const bool binds =
6415 (stmt["_type"] == "AnnAssign" && is_self_attr(stmt["target"])) ||
6416 (stmt["_type"] == "Assign" && is_self_attr(stmt["targets"][0]));
6417 if (!binds)
6418 continue;
6419 // The annotator rewrites `self.a = p` to `self.a: list = p`, so a bare
6420 // list annotation still takes T from the parameter.
6421 const Json elem = list_slice(stmt.value("annotation", Json()));
6422 const Json &value = stmt["value"];
6423 if (
6424 !elem.is_null() || !value.is_object() ||
6425 value.value("_type", "") != "Name")
6426 return elem;
6427 const Json param = find_annotated_assign(
6428 value["id"].template get<std::string>(), init["args"]["args"]);
6429 return param.empty() ? Json() : list_slice(param["annotation"]);
6430 }
6431 return Json();
6432}
6433
6438template <class Json>
6439void python_annotation<Json>::annotate_branch_blocks(Json &element)
6440{
6441 for (const char *field : {"orelse", "finalbody"})
6442 {
6443 if (!element.contains(field) || element[field].empty())
6444 continue;
6445 Json block = {{"body", element[field]}};
6446 add_annotation(block);
6447 element[field] = block["body"];
6448 }
6449 if (current_func_name_context_.empty() && element.contains("handlers"))
6450 for (Json &handler : element["handlers"])
6451 add_annotation(handler);
6452}
6453
6454template <class Json>
6455void python_annotation<Json>::add_annotation(Json &body)
6456{
6457 for (auto &element : body["body"])
6458 {
6459 auto itr = std::find(
6460 referenced_global_elements.begin(),
6461 referenced_global_elements.end(),
6462 element);
6463
6464 if (filter_global_elements_ && itr == referenced_global_elements.end())
6465 continue;
6466
6467 if (element.contains("lineno"))
6468 current_line_ = element["lineno"].template get<int>();
6469
6470 auto &stmt_type = element["_type"];
6471
6472 if (stmt_type == "If" || stmt_type == "While" || stmt_type == "Try")
6473 {
6474 // Type the loop variable first so the unpacking reads in the body see
6475 // its element type (#8928).
6476 if (stmt_type == "While")
6477 infer_loop_variable_types(element, true);
6478
6479 add_annotation(element);
6480
6481 // Infer loop variable types for preprocessor-transformed for loops
6482 if (stmt_type == "While")
6483 infer_loop_variable_types(element, false);
6484
6485 if (stmt_type != "While")
6486 annotate_branch_blocks(element);
6487
6488 continue;
6489 }
6490
6491 if (stmt_type == "FunctionDef")
6492 {
6493 // Only annotate nested functions, not the current function itself
6494 if (current_func != nullptr && &element != current_func)
6495 annotate_function(element);
6496
6497 continue;
6498 }
6499
6500 if (annotating_function_entry_point_)
6501 {
6502 if (
6503 stmt_type == "Expr" && element.contains("value") &&
6504 element["value"]["_type"] == "Call" &&
6505 element["value"]["func"]["_type"] == "Name")
6506 {
6507 // Best-effort forward-reference scan: the callee named here need
6508 // not be a top-level FunctionDef in this same ast_ (it could be a
6509 // builtin or something resolved elsewhere).
6510 const Json &top_level_body = ast_["body"];
6511 Json func_node = json_utils::try_find_function(
6512 top_level_body, element["value"]["func"]["id"]);
6513 if (!func_node.empty())
6514 add_annotation(func_node);
6515 }
6516 }
6517
6518 if (stmt_type != "Assign" || !element["type_comment"].is_null())
6519 continue;
6520
6521 // Skip tuple/list unpacking assignments
6522 // The C++ converter will handle them directly with proper type inference
6523 if (
6524 element.contains("targets") && !element["targets"].empty() &&
6525 element["targets"][0].contains("_type") &&
6526 (element["targets"][0]["_type"] == "Tuple" ||
6527 element["targets"][0]["_type"] == "List"))
6528 {
6529 continue;
6530 }
6531
6532 std::string inferred_type("");
6533
6534 // Check if RHS is a type identifier
6535 if (element["value"]["_type"] == "Name")
6536 {
6537 const std::string &rhs_name = element["value"]["id"];
6538 if (type_utils::is_type_identifier(rhs_name))
6539 {
6540 // This is a type object assignment: x = int
6541 inferred_type = "type";
6542 update_assignment_node(element, inferred_type);
6543 if (itr != referenced_global_elements.end())
6544 *itr = element;
6545 continue;
6546 }
6547 }
6548
6549 // Check if LHS was previously annotated
6550 if (element.contains("targets") && element["targets"][0]["_type"] == "Name")
6551 {
6552 inferred_type = get_type_from_lhs(element["targets"][0]["id"], body);
6553 }
6554
6555 if (infer_type(element, body, inferred_type) == InferResult::UNKNOWN)
6556 continue;
6557
6558 const auto &rhs = element["value"];
6559
6560 if (
6561 (rhs["_type"] == "Constant" && rhs["value"].is_null()) ||
6562 (rhs["_type"] == "NameConstant" && rhs["value"].is_null()) ||
6563 (rhs["_type"] == "Name" && rhs.contains("id") && rhs["id"] == "None"))
6564 inferred_type = "NoneType";
6565
6566 if (inferred_type.empty())
6567 {
6568 std::ostringstream oss;
6569 oss << "Type inference failed for "
6570 << stmt_type.template get<std::string>() << " at line "
6571 << current_line_;
6572
6573 throw std::runtime_error(oss.str());
6574 }
6575
6576 update_assignment_node(
6577 element, widen_to_optional_scalar(element, inferred_type));
6578 if (itr != referenced_global_elements.end())
6579 *itr = element;
6580 }
6581}
6582
6586template <class Json>
6587std::string python_annotation<Json>::widen_to_optional_scalar(
6588 const Json &element,
6589 const std::string &inferred_type)
6590{
6591 const Json &target = element["targets"][0];
6592 if (target["_type"] != "Name")
6593 return inferred_type;
6594 const Json &scope = current_func != nullptr ? *current_func : ast_;
6595 const auto key =
6596 std::make_pair(&scope, target["id"].template get<std::string>());
6597 auto it = optional_scalar_names_.find(key);
6598 if (it == optional_scalar_names_.end())
6599 it =
6600 optional_scalar_names_.emplace(key, optional_scalar_of(key.second, scope))
6601 .first;
6602 return it->second.empty() ? inferred_type : it->second;
6603}
6604
6608template <class Json>
6609std::string python_annotation<Json>::optional_scalar_of(
6610 const std::string &name,
6611 const Json &scope)
6612{
6613 auto stores_none = [](const Json *store) {
6614 const Json &value = (*store)["value"];
6615 return value.value("_type", "") == "Constant" && value["value"].is_null();
6616 };
6617 std::vector<const Json *> stores;
6618 collect_name_stores(name, scope["body"], stores);
6619 if (
6620 std::none_of(stores.begin(), stores.end(), stores_none) ||
6621 json_utils::count_name_bindings(scope, name) != stores.size())
6622 return "";
6623
6624 std::set<std::string> types;
6625 for (const Json *store : stores)
6626 {
6627 std::string type;
6628 if (stores_none(store))
6629 type = "NoneType";
6630 else
6631 infer_type(*store, scope, type);
6632 types.insert(type);
6633 }
6634 if (types.erase("NoneType") == 0 || types.size() != 1)
6635 return "";
6636 const std::string &scalar = *types.begin();
6637 return scalar == "int" || scalar == "float" || scalar == "bool"
6638 ? "Optional[" + scalar + "]"
6639 : "";
6640}
6641
6644template <class Json>
6645std::string python_annotation<Json>::returned_optional_scalar(
6646 const Json &function,
6647 const Json &body)
6648{
6649 for (const Json &stmt : body)
6650 {
6651 const std::string kind = stmt.value("_type", "");
6652 if (kind == "FunctionDef" || kind == "ClassDef")
6653 continue;
6654 if (
6655 kind == "Return" && stmt["value"].is_object() &&
6656 stmt["value"].value("_type", "") == "Name")
6657 {
6658 std::string optional = optional_scalar_of(
6659 stmt["value"]["id"].template get<std::string>(), function);
6660 if (!optional.empty())
6661 return optional;
6662 }
6663 for (const char *key : {"body", "orelse", "finalbody", "handlers", "cases"})
6664 if (stmt.contains(key) && stmt[key].is_array())
6665 if (
6666 std::string optional = returned_optional_scalar(function, stmt[key]);
6667 !optional.empty())
6668 return optional;
6669 }
6670 return "";
6671}
6672
6675template <class Json>
6676void python_annotation<Json>::collect_name_stores(
6677 const std::string &name,
6678 const Json &body,
6679 std::vector<const Json *> &stores)
6680{
6681 for (const Json &stmt : body)
6682 {
6683 const std::string kind = stmt.value("_type", "");
6684 if (kind == "FunctionDef" || kind == "ClassDef")
6685 continue;
6686 if (
6687 kind == "Assign" && stmt["targets"].size() == 1 &&
6688 stmt["targets"][0].value("_type", "") == "Name" &&
6689 stmt["targets"][0].value("id", "") == name)
6690 stores.push_back(&stmt);
6691 for (const char *key : {"body", "orelse", "finalbody", "handlers", "cases"})
6692 if (stmt.contains(key) && stmt[key].is_array())
6693 collect_name_stores(name, stmt[key], stores);
6694 }
6695}
static void count_attr_writers(const Json &node, const std::string &attr, const std::string &name, size_t &stores, size_t &name_reads)
Counts the nodes under node that may store to an attribute named attr, a setattr call or a __class__ ...
Definition annotation_conversion.inl:1285
static Json find_first_append_arg(const Json &node, const std::string &name)
Definition annotation_conversion.inl:1389
static bool call_site_types_agree(const std::string &a, const std::string &b)
Whether two call-site types agree: equal, or the same container where one spells only the bare kind (...
Definition annotation_conversion.inl:5057
std::string unpacked_name_element_type(const std::string &type, size_t index)
The type of element index of a value typed type: the matching parameter of tuple[....
Definition annotation_conversion.inl:256
static bool rebound_in_function(const std::string &name, const std::string &ctx, const Json &ast)
Whether the function at ctx, or the enclosing one that binds name, assigns name after binding it,...
Definition annotation_conversion.inl:4946
static bool all_pointer_sized_scalars(const std::set< std::string > &types)
Whether types is non-empty and holds only types whose values an untyped (pointer-sized) return slot c...
Definition annotation_conversion.inl:4135
bool is_unresolved_arg_type(const std::string &type)
True when an argument type says too little to annotate a parameter with (see collect_function_call_ar...
Definition annotation_conversion.inl:747
bool has_bare_tuple(const std::string &type)
True when type spells a tuple without its element types anywhere.
Definition annotation_conversion.inl:731
static bool union_has_none(const Json &slice)
Whether a Union[...] slice lists None among its members.
Definition annotation_conversion.inl:1965
bool is_empty_list_literal(const Json &node)
True when node binds the empty list literal [], which is how a comprehension's result list starts bef...
Definition annotation_conversion.inl:720
static const char *const untyped_name_return
Marks a return <name> whose type is unknown. Not a valid type name.
Definition annotation_conversion.inl:4131
static bool is_dynamically_typed_name(const Json &arg, const Json &scope)
A bare name that scope assigns incompatible literal kinds, which must reach a method as a tagged valu...
Definition annotation_conversion.inl:5475
std::string subscript_result_type(const std::string &container, bool is_slice)
The type a subscript of a container-typed value yields, or empty.
Definition annotation_conversion.inl:701
static bool aliases_function(const Json &stmt, const std::string &name, const Json &body)
True when stmt aliases name to a function defined in body.
Definition annotation_conversion.inl:5178
static bool name_only_indexed(const Json &node, const std::string &name)
Whether every read of name under node indexes it (name[i]), so no store, method call or alias can cha...
Definition annotation_conversion.inl:1541
bool is_imported_numpy_module_alias(const nlohmann::json &ast, const std::string &name)
Definition converter_stmt.cpp:756
void base_type(type2tc &type, const namespacet &ns)
Definition base_type.cpp:6
static const twodig_t base
Definition bigint.cpp:28
The functions a Python expression may evaluate to, gathered from every binding of the names it reads:...
Definition function_values.h:18
Definition module.h:60
function get_function(const std::string &func_name) const
Retrieve a function definition by name.
Definition module.h:151
bool star_import_binds(const std::string &name) const
Whether from <this module> import * binds name: a public name, and no mention of __all__,...
Definition module.h:190
static std::string list_annotation(const nlohmann::json &call, const nlohmann::json &ast)
"list" for a random call that returns an array, so the annotator types it and arithmetic on it as it ...
Definition numpy_random.cpp:247
void collect_function_call_arg_types(Json &node, std::map< std::pair< std::string, size_t >, std::set< std::string > > &param_types, const std::set< std::string > &top_level_funcs, std::set< std::pair< std::string, size_t > > &dynamically_typed_params)
Definition annotation_conversion.inl:5836
bool is_instance_call(const Json &func) const
Whether func names neither a module class nor a module function, so a call through it can only reach ...
Definition annotation_conversion.inl:5657
void add_extra_subscript_inference_source(const Json &ast)
Definition annotation_conversion.inl:5979
void preprocess_method_calls(const Json &node)
Definition annotation_conversion.inl:5571
void add_type_annotation()
Definition annotation_conversion.inl:5931
Json * class_method(const std::string &cls, const Json &method)
The FunctionDef of method in module-level class cls, or null.
Definition annotation_conversion.inl:5644
std::string promote_to_float_operand(const Json &stmt, const std::string &type)
True division of an int or bool yields a float, and so does other arithmetic with a float operand,...
Definition annotation_conversion.inl:1931
bool annotate_from_call_sites(Json &root, Json &param, const std::pair< std::string, size_t > &key, std::set< std::string > types, const std::set< std::string > &top_level_funcs)
Annotates param when its call sites agree on one type.
Definition annotation_conversion.inl:5810
bool preprocess_constructor_calls(Json &node)
Definition annotation_conversion.inl:5483
static const std::string & unresolved_arg_type()
Sentinel recorded for a call-site argument this pass cannot type; never a valid Python type name,...
Definition python_annotation.h:77
bool preprocess_function_calls(Json &root)
Infer unannotated parameter types of top-level functions from the argument types at every plain f(arg...
Definition annotation_conversion.inl:5753
void preprocess_receiver_method_calls()
Types the unannotated parameters of a method from its obj.m(args) calls at module level,...
Definition annotation_conversion.inl:5671
static std::optional< bool > binding_is_import(const nlohmann::json &module, const scope_bindings &global, const nlohmann::json *function, bool in_class, bool in_order, const std::string &name, const nlohmann::json &use)
Whether name, read at use inside function (null at module or class level), reads a binding made by an...
Definition converter_symbols.cpp:535
static scope_bindings module_scope_bindings(const nlohmann::json &module, const std::string &name)
The bindings of name in module's own scope, with declared_outer meaning that a function rebinds it th...
Definition converter_symbols.cpp:558
static bool is_type_identifier(const std::string &name)
Definition type_utils.h:451
static bool is_python_exceptions(const std::string &name)
Definition type_utils.h:170
static bool is_python_model_func(const std::string &name)
Definition type_utils.h:161
static std::string get_type_from_consensus_func(const std::string &name)
Definition type_utils.h:145
static bool is_consensus_type(const std::string &name)
Definition type_utils.h:116
static bool is_consensus_func(const std::string &name)
Definition type_utils.h:139
static bool is_builtin_type(const std::string &name)
Definition type_utils.h:86
Definition toml.hpp:1531
configt config
Definition config.cpp:8
int count
Definition cuda.h:51
@ full
Alpha-rename every symbol: the digest sees structure and types only.
Definition fingerprint.h:23
literalt pos(literalt a)
Definition literal.h:130
#define log_warning(fmt,...)
Definition message.h:89
float e
Definition cmath.py:18
field(*args, **kwargs)
Definition dataclasses.py:28
bool scope_assigns_divergent_literal_types(const std::string &name, const nlohmann::json &scope_body, const nlohmann::json &module_body=nlohmann::json())
Definition literal_divergence.h:347
Definition json_utils.h:18
const JsonType get_var_node(const std::string &var_name, const JsonType &block)
Definition json_utils.h:704
JsonType find_class(const JsonType &ast_json, const std::string &class_name)
Definition json_utils.h:159
const JsonType find_var_decl(const std::string &var_name, const std::string &function, const JsonType &ast)
Definition json_utils.h:918
std::vector< std::string > split_top_level_params(const std::string &s)
Split a rendered parameter list on commas that are not inside brackets, trimming each part.
Definition json_utils.h:982
JsonType find_function_by_path(const JsonType &ast, const std::vector< std::string > &path)
Definition json_utils.h:834
bool is_class(const std::string &name, const JsonType &ast_json)
Definition json_utils.h:441
JsonType find_var_in_function(const std::string &var_name, const JsonType &func_node)
Definition json_utils.h:888
std::size_t count_name_bindings(const nlohmann::json &node, const std::string &name)
Definition json_utils.h:1092
bool ast_equal_ignoring_location(const nlohmann::json &a, const nlohmann::json &b)
Definition json_utils.h:74
std::string get_annotation_type_name(const JsonType &annotation)
Definition json_utils.h:957
std::vector< std::string > split_function_path(const std::string &function)
Definition json_utils.h:792
std::string render_annotation_type(const JsonType &annotation)
The full generic spelling of an annotation node, e.g.
Definition json_utils.h:1012
std::string constructed_attr_class(const Json &class_node, const std::string &attr)
C when the __init__ of class_node holds self.
Definition json_utils.h:229
JsonType & find_function_or_throw(JsonType &json, const std::string &func_name)
Throws std::runtime_error on a miss.
Definition json_utils.h:562
const std::string get_object_alias(const JsonType &ast, const std::string &obj_name)
Definition json_utils.h:675
bool has_overload_decorator(const JsonType &func_node)
Definition json_utils.h:1367
std::size_t count_attr_stores(const Json &node, const std::string &attr, bool non_self_only)
Counts the stores and deletions of <obj>.
Definition json_utils.h:211
bool class_defines_method(const JsonType &ast_body, const std::string &class_name, const std::string &method_name)
True when class_name or one of its (transitive) base classes defines a method named method_name.
Definition json_utils.h:338
bool has_multiple_assignments_in_block(const std::string &var_name, const JsonType &block)
Definition json_utils.h:1134
const JsonType find_return_node(const JsonType &block)
Definition json_utils.h:1312
const JsonType & find_imported_function(const JsonType &ast, const std::string &func_name)
Definition json_utils.h:596
JsonType try_find_function(const JsonType &json, const std::string &func_name)
Returns an empty JsonType on a miss. Never throws.
Definition json_utils.h:550
Json find_attr_annotation(const Json &class_node, const std::string &attr)
The annotation of self.
Definition json_utils.h:173
list[float] empty(int shape)
Definition numpy.py:29
list[tuple[str, list[str], list[str]]] walk(str top)
Definition os.py:48
lineno
Definition threading_lowering.py:600
body
Definition threading_lowering.py:616
Definition path.py:1
nlohmann::json find_self_attr_init_rhs(const std::string &cls, const std::string &attr, const nlohmann::json &ast_body)
Definition annotation_parser.cpp:66
Definition random.py:1
Definition exception.cpp:51
Definition string.py:1
type
Definition transform.py:9
p
Definition transform.py:12
name
Definition transform.py:21
end
Definition transform.py:37
args
Definition transform.py:10
static std::string node_name(const nlohmann::json &node)
Definition expr.cpp:520
std::string get_base_var_name(const nlohmann::json &node)
Definition annotation_utils.cpp:63
nlohmann::json find_function_recursive(const std::string &func_name, const nlohmann::json &body)
Definition annotation_parser.cpp:26
const std::map< std::string, std::string > & builtin_functions()
Definition annotation_intrinsics.cpp:5
bool has_return_none(const nlohmann::json &body)
Definition annotation_utils.cpp:36
std::string get_type_from_json(const nlohmann::json &value)
Definition annotation_utils.cpp:9
std::string invert_substrings(const std::string &input)
Definition annotation_utils.cpp:78
bool has_annotation(const nlohmann::json &node)
Definition annotation_utils.cpp:30
std::string infer_type_from_default_arg_shape(const nlohmann::json &args_node)
Definition annotation_utils.cpp:103
nlohmann::json find_lambda_in_body(const std::string &func_name, const nlohmann::json &body)
Definition annotation_parser.cpp:10
InferResult
Definition annotation_utils.h:19
void * value
Definition solidity_mapping.c:2
uint256_t key
Definition solidity_mapping.c:1
unsigned stack_budget_guardt< Tag >::depth
Definition stack_budget.h:72
#define bool
Definition stdbool.h:9
Definition module.h:38
std::string name_
Definition module.h:39
A function definition and the module AST it belongs to.
Definition function_values.h:22
Definition module.h:12
std::string name_
Definition module.h:13
std::string return_type_
Definition module.h:14
constexpr bool is_string
Definition toml.hpp:2379