@pineforge/codegen-pyodide 0.7.6 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,560 @@
1
+ """Drawing-objects-as-data dispatch for the codegen.
2
+
3
+ Spec: ``docs/drawing-objects-as-data.md`` §4.3 (the 4-form call dispatch),
4
+ §L (handle lifecycle) and §U (UDT × drawing). Everything here only ever
5
+ runs for a strategy whose ``self._uses_drawing`` is True (the dispatch is
6
+ reached solely when a ``line``/``box``/``label``/``linefill``/``chart.point``
7
+ call or a drawing-typed receiver is present), so non-drawing strategies emit
8
+ byte-identical C++.
9
+
10
+ The geometry of a drawing object becomes REAL C++ state (per-type arena in
11
+ ``pineforge/drawing.hpp``); every VISUAL kwarg/method (color / style / width /
12
+ bgcolor / text_* / extend-as-visual / force_overlay) is accepted-and-DROPPED.
13
+
14
+ ``DrawingVisitor`` is mixed into ``CodeGen``; it owns no state of its own and
15
+ reads only attributes established by ``CodeGen.__init__`` (``_uses_drawing``,
16
+ ``_udt_var_types``, ``_udt_param_udt``) and sibling mixins
17
+ (``_visit_expr``, ``_type_spec_from_expr``).
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ from ..ast_nodes import FuncCall, Identifier, MemberAccess
23
+ from .tables import DRAWING_TYPE_TO_CPP, DRAWING_ARENA, ARRAY_VOID_METHODS as _ARRAY_VOID_METHODS
24
+
25
+ # ---------------------------------------------------------------------------
26
+ # Canonical Pine v6 constructor param-name lists (positional order).
27
+ # Only the GEOMETRY names are consumed downstream; every other (visual) name is
28
+ # merged-and-dropped. Two overloads each: scalar coords vs chart.point.
29
+ # ---------------------------------------------------------------------------
30
+ _LINE_CTOR = ["x1", "y1", "x2", "y2", "xloc", "extend", "color", "style",
31
+ "width", "force_overlay"]
32
+ _LINE_CTOR_PTS = ["first_point", "second_point", "xloc", "color", "style",
33
+ "width", "extend", "force_overlay"]
34
+ _BOX_CTOR = ["left", "top", "right", "bottom", "border_color", "border_width",
35
+ "border_style", "extend", "xloc", "bgcolor", "text", "text_size",
36
+ "text_color", "text_halign", "text_valign", "text_wrap",
37
+ "text_font_family", "force_overlay"]
38
+ _BOX_CTOR_PTS = ["top_left", "bottom_right", "border_color", "border_width",
39
+ "border_style", "extend", "xloc", "bgcolor", "text",
40
+ "text_size", "text_color", "text_halign", "text_valign",
41
+ "text_wrap", "text_font_family", "force_overlay"]
42
+ _LABEL_CTOR = ["x", "y", "text", "xloc", "yloc", "color", "style", "textcolor",
43
+ "size", "textalign", "tooltip", "text_font_family",
44
+ "force_overlay"]
45
+ _LABEL_CTOR_PTS = ["point", "text", "xloc", "yloc", "color", "style",
46
+ "textcolor", "size", "textalign", "tooltip",
47
+ "text_font_family", "force_overlay"]
48
+
49
+
50
+ # ---------------------------------------------------------------------------
51
+ # Method classification per type. GEOMETRY methods emit real arena calls;
52
+ # *_NOOP visual setters emit pf_noop (args evaluated + discarded).
53
+ # ---------------------------------------------------------------------------
54
+ LINE_METHODS = frozenset({
55
+ "get_x1", "get_x2", "get_y1", "get_y2", "get_price",
56
+ "set_x1", "set_x2", "set_y1", "set_y2", "set_xy1", "set_xy2",
57
+ "set_first_point", "set_second_point", "set_xloc", "copy", "delete",
58
+ })
59
+ LINE_NOOP = frozenset({"set_color", "set_style", "set_width", "set_extend"})
60
+
61
+ BOX_METHODS = frozenset({
62
+ "get_left", "get_right", "get_top", "get_bottom",
63
+ "set_left", "set_right", "set_top", "set_bottom",
64
+ "set_lefttop", "set_rightbottom",
65
+ "set_top_left_point", "set_bottom_right_point", "set_xloc",
66
+ "copy", "delete",
67
+ })
68
+ BOX_NOOP = frozenset({
69
+ "set_border_color", "set_border_width", "set_border_style", "set_bgcolor",
70
+ "set_extend", "set_text", "set_text_color", "set_text_size",
71
+ "set_text_halign", "set_text_valign", "set_text_font_family",
72
+ "set_text_wrap", "set_text_formatting",
73
+ })
74
+
75
+ LABEL_METHODS = frozenset({
76
+ "get_x", "get_y", "get_text",
77
+ "set_x", "set_y", "set_xy", "set_point", "set_xloc", "set_yloc",
78
+ "set_text", "copy", "delete",
79
+ })
80
+ LABEL_NOOP = frozenset({
81
+ "set_color", "set_style", "set_textcolor", "set_size", "set_textalign",
82
+ "set_tooltip", "set_text_font_family", "set_text_formatting",
83
+ })
84
+
85
+ LINEFILL_METHODS = frozenset({"get_line1", "get_line2", "delete"})
86
+ LINEFILL_NOOP = frozenset({"set_color"})
87
+
88
+ # Scalar getter -> C++ return type. (linefill.get_line1/2 return a Line handle,
89
+ # typed via _type_spec_from_expr -> udt, not here.) x-coords are int64_t,
90
+ # y-coords/prices double, label text std::string.
91
+ _DRAWING_GETTER_RET = {
92
+ ("line", "get_x1"): "int64_t", ("line", "get_x2"): "int64_t",
93
+ ("line", "get_y1"): "double", ("line", "get_y2"): "double",
94
+ ("line", "get_price"): "double",
95
+ ("box", "get_left"): "int64_t", ("box", "get_right"): "int64_t",
96
+ ("box", "get_top"): "double", ("box", "get_bottom"): "double",
97
+ ("label", "get_x"): "int64_t", ("label", "get_y"): "double",
98
+ ("label", "get_text"): "std::string",
99
+ }
100
+
101
+ # Per-type lookup (geometry, noop) used by the dispatcher and support_checker.
102
+ DRAWING_METHODS_BY_TYPE = {
103
+ "line": (LINE_METHODS, LINE_NOOP),
104
+ "box": (BOX_METHODS, BOX_NOOP),
105
+ "label": (LABEL_METHODS, LABEL_NOOP),
106
+ "linefill": (LINEFILL_METHODS, LINEFILL_NOOP),
107
+ }
108
+
109
+ # All method names that the §4.3 receiver dispatch recognises. ``new`` is
110
+ # DELIBERATELY excluded (it is namespace-functional only and would otherwise
111
+ # shadow ``Type.new(...)`` UDT constructors). Gating the receiver branch on
112
+ # membership here enforces the L.1 precedence rule: a user method (egoigor's
113
+ # ``slope``) is not in this set and therefore routes to user-method dispatch.
114
+ ALL_DRAWING_METHODS = (
115
+ LINE_METHODS | LINE_NOOP | BOX_METHODS | BOX_NOOP
116
+ | LABEL_METHODS | LABEL_NOOP | LINEFILL_METHODS | LINEFILL_NOOP
117
+ )
118
+
119
+
120
+ class DrawingVisitor:
121
+ """Drawing-objects-as-data emit helpers. Mixed into ``CodeGen``."""
122
+
123
+ # ------------------------------------------------------------------
124
+ # Enum / extend lowering helpers
125
+ # ------------------------------------------------------------------
126
+ @staticmethod
127
+ def _lower_xloc(node) -> str:
128
+ if (isinstance(node, MemberAccess) and isinstance(node.object, Identifier)
129
+ and node.object.name == "xloc" and node.member == "bar_time"):
130
+ return "XLoc::bar_time"
131
+ return "XLoc::bar_index"
132
+
133
+ @staticmethod
134
+ def _lower_yloc(node) -> str:
135
+ if isinstance(node, MemberAccess) and isinstance(node.object, Identifier) and node.object.name == "yloc":
136
+ if node.member == "abovebar":
137
+ return "YLoc::abovebar"
138
+ if node.member == "belowbar":
139
+ return "YLoc::belowbar"
140
+ return "YLoc::price"
141
+
142
+ @staticmethod
143
+ def _lower_extend(node) -> tuple[str, str]:
144
+ """Map ``extend.{none,left,right,both}`` -> (ext_left, ext_right)."""
145
+ if isinstance(node, MemberAccess) and isinstance(node.object, Identifier) and node.object.name == "extend":
146
+ return {
147
+ "both": ("true", "true"),
148
+ "left": ("true", "false"),
149
+ "right": ("false", "true"),
150
+ "none": ("false", "false"),
151
+ }.get(node.member, ("false", "false"))
152
+ return ("false", "false")
153
+
154
+ @staticmethod
155
+ def _is_chart_point_callee(callee) -> bool:
156
+ """True for ``chart.point.<member>(...)`` callees."""
157
+ return (
158
+ isinstance(callee, MemberAccess)
159
+ and isinstance(callee.object, MemberAccess)
160
+ and isinstance(callee.object.object, Identifier)
161
+ and callee.object.object.name == "chart"
162
+ and callee.object.member == "point"
163
+ )
164
+
165
+ # ------------------------------------------------------------------
166
+ # Constructor (CREATE) lowering
167
+ # ------------------------------------------------------------------
168
+ @staticmethod
169
+ def _merge_drawing_args(node: FuncCall, param_names: list[str]) -> dict:
170
+ """Map positional args + kwargs onto ``param_names`` -> {name: node}."""
171
+ vals: dict = {}
172
+ for i, a in enumerate(node.args):
173
+ if i < len(param_names):
174
+ vals[param_names[i]] = a
175
+ for k, v in node.kwargs.items():
176
+ vals[k] = v
177
+ return vals
178
+
179
+ def _drawing_ctor_is_points(self, node: FuncCall) -> bool:
180
+ """Pick the chart.point overload when the first geometry arg is a
181
+ ChartPoint (inferred TypeSpec udt('chart.point')) or a point kwarg is
182
+ supplied."""
183
+ for k in ("first_point", "top_left", "point"):
184
+ if k in node.kwargs:
185
+ return True
186
+ if node.args:
187
+ spec = self._type_spec_from_expr(node.args[0])
188
+ if spec is not None and spec.kind == "udt" and spec.name == "chart.point":
189
+ return True
190
+ return False
191
+
192
+ def _emit_drawing_ctor(self, dtype: str, node: FuncCall) -> str:
193
+ arena = DRAWING_ARENA[dtype]
194
+ if dtype == "linefill":
195
+ vals = self._merge_drawing_args(node, ["line1", "line2", "color"])
196
+ l1 = self._visit_expr(vals["line1"]) if vals.get("line1") is not None else "Line{}"
197
+ l2 = self._visit_expr(vals["line2"]) if vals.get("line2") is not None else "Line{}"
198
+ return f"pf_linefill_new({arena}, {l1}, {l2})"
199
+
200
+ use_pts = self._drawing_ctor_is_points(node)
201
+
202
+ if dtype == "line":
203
+ vals = self._merge_drawing_args(node, _LINE_CTOR_PTS if use_pts else _LINE_CTOR)
204
+ xloc = self._lower_xloc(vals.get("xloc"))
205
+ if use_pts:
206
+ p1 = self._visit_expr(vals.get("first_point"))
207
+ p2 = self._visit_expr(vals.get("second_point"))
208
+ return f"pf_line_new_pts({arena}, {p1}, {p2}, {xloc})"
209
+ el, er = self._lower_extend(vals.get("extend"))
210
+ x1 = self._visit_expr(vals.get("x1"))
211
+ y1 = self._visit_expr(vals.get("y1"))
212
+ x2 = self._visit_expr(vals.get("x2"))
213
+ y2 = self._visit_expr(vals.get("y2"))
214
+ return (f"pf_line_new({arena}, (int64_t)({x1}), (double)({y1}), "
215
+ f"(int64_t)({x2}), (double)({y2}), {xloc}, {el}, {er})")
216
+
217
+ if dtype == "box":
218
+ vals = self._merge_drawing_args(node, _BOX_CTOR_PTS if use_pts else _BOX_CTOR)
219
+ xloc = self._lower_xloc(vals.get("xloc"))
220
+ if use_pts:
221
+ tl = self._visit_expr(vals.get("top_left"))
222
+ br = self._visit_expr(vals.get("bottom_right"))
223
+ return f"pf_box_new_pts({arena}, {tl}, {br}, {xloc})"
224
+ left = self._visit_expr(vals.get("left"))
225
+ top = self._visit_expr(vals.get("top"))
226
+ right = self._visit_expr(vals.get("right"))
227
+ bottom = self._visit_expr(vals.get("bottom"))
228
+ return (f"pf_box_new({arena}, (int64_t)({left}), (double)({top}), "
229
+ f"(int64_t)({right}), (double)({bottom}), {xloc})")
230
+
231
+ if dtype == "label":
232
+ vals = self._merge_drawing_args(node, _LABEL_CTOR_PTS if use_pts else _LABEL_CTOR)
233
+ yloc = self._lower_yloc(vals.get("yloc"))
234
+ text = (self._visit_expr(vals["text"]) if vals.get("text") is not None
235
+ else 'std::string("")')
236
+ if use_pts:
237
+ pt = self._visit_expr(vals.get("point"))
238
+ return f"pf_label_new_pt({arena}, {pt}, {text}, {yloc})"
239
+ x = self._visit_expr(vals.get("x"))
240
+ y = self._visit_expr(vals.get("y"))
241
+ xloc = self._lower_xloc(vals.get("xloc"))
242
+ return (f"pf_label_new({arena}, (int64_t)({x}), (double)({y}), "
243
+ f"{text}, {xloc}, {yloc})")
244
+
245
+ return "0" # unreachable
246
+
247
+ # ------------------------------------------------------------------
248
+ # chart.point — inline aggregate literals (no arena)
249
+ # ------------------------------------------------------------------
250
+ def _emit_chart_point(self, func_name: str, node: FuncCall) -> str:
251
+ if func_name == "copy":
252
+ inner = self._visit_expr(node.args[0]) if node.args else "ChartPoint{}"
253
+ return f"ChartPoint({inner})"
254
+ if func_name == "now":
255
+ vals = self._merge_drawing_args(node, ["price"])
256
+ price = (self._visit_expr(vals["price"]) if vals.get("price") is not None
257
+ else "current_bar_.close")
258
+ return (f"ChartPoint{{ .index=bar_index_, "
259
+ f".time=(int64_t)current_bar_.timestamp, .price=({price}) }}")
260
+ if func_name == "from_index":
261
+ vals = self._merge_drawing_args(node, ["index", "price"])
262
+ idx = self._visit_expr(vals.get("index"))
263
+ price = self._visit_expr(vals.get("price"))
264
+ return (f"ChartPoint{{ .index=(int64_t)({idx}), .time=na<int64_t>(), "
265
+ f".price=({price}) }}")
266
+ if func_name == "from_time":
267
+ vals = self._merge_drawing_args(node, ["time", "price"])
268
+ tm = self._visit_expr(vals.get("time"))
269
+ price = self._visit_expr(vals.get("price"))
270
+ return (f"ChartPoint{{ .index=na<int64_t>(), .time=(int64_t)({tm}), "
271
+ f".price=({price}) }}")
272
+ if func_name == "new":
273
+ vals = self._merge_drawing_args(node, ["time", "index", "price"])
274
+ tm = self._visit_expr(vals.get("time"))
275
+ idx = self._visit_expr(vals.get("index"))
276
+ price = self._visit_expr(vals.get("price"))
277
+ return (f"ChartPoint{{ .index=(int64_t)({idx}), .time=(int64_t)({tm}), "
278
+ f".price=({price}) }}")
279
+ return "ChartPoint{}"
280
+
281
+ # ------------------------------------------------------------------
282
+ # Namespace-functional + method-form dispatch
283
+ # ------------------------------------------------------------------
284
+ def _emit_drawing_namespace_call(self, namespace: str, func_name: str, node: FuncCall) -> str:
285
+ """``line.new(...)`` / ``line.get_y2(ln)`` / ``linefill.new(l1,l2,c)`` …"""
286
+ if func_name == "new":
287
+ return self._emit_drawing_ctor(namespace, node)
288
+ if not node.args:
289
+ return "0"
290
+ return self._emit_drawing_method(namespace, func_name, node.args[0], list(node.args[1:]), node)
291
+
292
+ def _emit_drawing_method(self, dtype: str, method: str, recv_node, arg_nodes: list, node: FuncCall) -> str:
293
+ """Lower a drawing method onto its arena (or pf_noop for visual setters).
294
+
295
+ ``recv_node`` is the handle receiver; ``arg_nodes`` the remaining args.
296
+ """
297
+ arena = DRAWING_ARENA[dtype]
298
+ recv = self._visit_expr(recv_node)
299
+ geometry, noop = DRAWING_METHODS_BY_TYPE[dtype]
300
+ if method in noop:
301
+ extra = "".join(", " + self._visit_expr(a) for a in arg_nodes)
302
+ return f"pf_noop({recv}{extra})"
303
+ av = [self._visit_expr(a) for a in arg_nodes]
304
+ if dtype == "line":
305
+ return self._emit_line_method(method, arena, recv, av, arg_nodes)
306
+ if dtype == "box":
307
+ return self._emit_box_method(method, arena, recv, av, arg_nodes)
308
+ if dtype == "label":
309
+ return self._emit_label_method(method, arena, recv, av, arg_nodes)
310
+ if dtype == "linefill":
311
+ return self._emit_linefill_method(method, arena, recv, av)
312
+ return "0"
313
+
314
+ def _emit_line_method(self, m, a, r, av, raw) -> str:
315
+ if m == "get_x1":
316
+ return f"pf_line_get_x1({a}, {r})"
317
+ if m == "get_x2":
318
+ return f"pf_line_get_x2({a}, {r})"
319
+ if m == "get_y1":
320
+ return f"pf_line_get_y1({a}, {r})"
321
+ if m == "get_y2":
322
+ return f"pf_line_get_y2({a}, {r})"
323
+ if m == "get_price":
324
+ return f"pf_line_get_price({a}, {r}, (int64_t)({av[0]}))"
325
+ if m == "set_x1":
326
+ return f"pf_line_set_x1({a}, {r}, (int64_t)({av[0]}))"
327
+ if m == "set_x2":
328
+ return f"pf_line_set_x2({a}, {r}, (int64_t)({av[0]}))"
329
+ if m == "set_y1":
330
+ return f"pf_line_set_y1({a}, {r}, (double)({av[0]}))"
331
+ if m == "set_y2":
332
+ return f"pf_line_set_y2({a}, {r}, (double)({av[0]}))"
333
+ if m == "set_xy1":
334
+ return f"pf_line_set_xy1({a}, {r}, (int64_t)({av[0]}), (double)({av[1]}))"
335
+ if m == "set_xy2":
336
+ return f"pf_line_set_xy2({a}, {r}, (int64_t)({av[0]}), (double)({av[1]}))"
337
+ if m == "set_first_point":
338
+ return f"pf_line_set_first_point({a}, {r}, {av[0]})"
339
+ if m == "set_second_point":
340
+ return f"pf_line_set_second_point({a}, {r}, {av[0]})"
341
+ if m == "set_xloc":
342
+ return f"pf_line_set_xloc({a}, {r}, (int64_t)({av[0]}), (int64_t)({av[1]}), {self._lower_xloc(raw[2])})"
343
+ if m == "copy":
344
+ return f"pf_line_copy({a}, {r})"
345
+ if m == "delete":
346
+ return f"pf_line_delete({a}, {r})"
347
+ return "0"
348
+
349
+ def _emit_box_method(self, m, a, r, av, raw) -> str:
350
+ if m == "get_left":
351
+ return f"pf_box_get_left({a}, {r})"
352
+ if m == "get_right":
353
+ return f"pf_box_get_right({a}, {r})"
354
+ if m == "get_top":
355
+ return f"pf_box_get_top({a}, {r})"
356
+ if m == "get_bottom":
357
+ return f"pf_box_get_bottom({a}, {r})"
358
+ if m == "set_left":
359
+ return f"pf_box_set_left({a}, {r}, (int64_t)({av[0]}))"
360
+ if m == "set_right":
361
+ return f"pf_box_set_right({a}, {r}, (int64_t)({av[0]}))"
362
+ if m == "set_top":
363
+ return f"pf_box_set_top({a}, {r}, (double)({av[0]}))"
364
+ if m == "set_bottom":
365
+ return f"pf_box_set_bottom({a}, {r}, (double)({av[0]}))"
366
+ if m == "set_lefttop":
367
+ return f"pf_box_set_lefttop({a}, {r}, (int64_t)({av[0]}), (double)({av[1]}))"
368
+ if m == "set_rightbottom":
369
+ return f"pf_box_set_rightbottom({a}, {r}, (int64_t)({av[0]}), (double)({av[1]}))"
370
+ if m == "set_top_left_point":
371
+ return f"pf_box_set_top_left_point({a}, {r}, {av[0]})"
372
+ if m == "set_bottom_right_point":
373
+ return f"pf_box_set_bottom_right_point({a}, {r}, {av[0]})"
374
+ if m == "set_xloc":
375
+ return f"pf_box_set_xloc({a}, {r}, (int64_t)({av[0]}), (int64_t)({av[1]}), {self._lower_xloc(raw[2])})"
376
+ if m == "copy":
377
+ return f"pf_box_copy({a}, {r})"
378
+ if m == "delete":
379
+ return f"pf_box_delete({a}, {r})"
380
+ return "0"
381
+
382
+ def _emit_label_method(self, m, a, r, av, raw) -> str:
383
+ if m == "get_x":
384
+ return f"pf_label_get_x({a}, {r})"
385
+ if m == "get_y":
386
+ return f"pf_label_get_y({a}, {r})"
387
+ if m == "get_text":
388
+ return f"pf_label_get_text({a}, {r})"
389
+ if m == "set_x":
390
+ return f"pf_label_set_x({a}, {r}, (int64_t)({av[0]}))"
391
+ if m == "set_y":
392
+ return f"pf_label_set_y({a}, {r}, (double)({av[0]}))"
393
+ if m == "set_xy":
394
+ return f"pf_label_set_xy({a}, {r}, (int64_t)({av[0]}), (double)({av[1]}))"
395
+ if m == "set_point":
396
+ return f"pf_label_set_point({a}, {r}, {av[0]})"
397
+ if m == "set_xloc":
398
+ return f"pf_label_set_xloc({a}, {r}, (int64_t)({av[0]}), {self._lower_xloc(raw[1])})"
399
+ if m == "set_yloc":
400
+ return f"pf_label_set_yloc({a}, {r}, {self._lower_yloc(raw[0])})"
401
+ if m == "set_text":
402
+ return f"pf_label_set_text({a}, {r}, {av[0]})"
403
+ if m == "copy":
404
+ return f"pf_label_copy({a}, {r})"
405
+ if m == "delete":
406
+ return f"pf_label_delete({a}, {r})"
407
+ return "0"
408
+
409
+ def _emit_linefill_method(self, m, a, r, av) -> str:
410
+ if m == "get_line1":
411
+ return f"pf_linefill_get_line1({a}, {r})"
412
+ if m == "get_line2":
413
+ return f"pf_linefill_get_line2({a}, {r})"
414
+ if m == "delete":
415
+ return f"pf_linefill_delete({a}, {r})"
416
+ return "0"
417
+
418
+ def _drawing_call_return_cpp(self, node: FuncCall) -> str | None:
419
+ """C++ scalar return type for a drawing GETTER call, else None.
420
+
421
+ Covers both the namespace-functional form (``label.get_text(lb)``) and
422
+ the method form (``lb.get_text()``). Used by ``_infer_type`` /
423
+ ``_type_for_decl`` so the receiving local declares as the right scalar
424
+ (an int64_t x-coord, a double y-coord, or a std::string label text)
425
+ instead of the analyzer's default double.
426
+ """
427
+ callee = node.callee
428
+ if not isinstance(callee, MemberAccess):
429
+ return None
430
+ method = callee.member
431
+ _fn, ns = self._resolve_callee(callee)
432
+ from .tables import DRAWING_NS
433
+ dtype = None
434
+ if ns in DRAWING_NS:
435
+ dtype = ns
436
+ else:
437
+ recv_spec = self._type_spec_from_expr(callee.object)
438
+ if (recv_spec is not None and recv_spec.kind == "udt"
439
+ and recv_spec.name in DRAWING_TYPE_TO_CPP):
440
+ dtype = recv_spec.name
441
+ if dtype is None:
442
+ return None
443
+ return _DRAWING_GETTER_RET.get((dtype, method))
444
+
445
+ def _drawing_call_is_void(self, node) -> bool:
446
+ """True if ``node`` is a drawing call that lowers to a VOID C++
447
+ expression (setters, ``delete``, visual-noop) — i.e. it cannot be used
448
+ as a return value / RHS. Constructors (``new``), ``copy`` and getters
449
+ return a value. Used by UDF last-expression lowering so a function whose
450
+ final statement is e.g. ``label.set_text(lb, ...)`` emits it as a
451
+ statement instead of ``return pf_label_set_text(...);``.
452
+ """
453
+ if not isinstance(node, FuncCall) or not isinstance(node.callee, MemberAccess):
454
+ return False
455
+ method = node.callee.member
456
+ _fn, ns = self._resolve_callee(node.callee)
457
+ from .tables import DRAWING_NS
458
+ dtype = None
459
+ if ns in DRAWING_NS:
460
+ dtype = ns
461
+ else:
462
+ recv_spec = self._type_spec_from_expr(node.callee.object)
463
+ if (recv_spec is not None and recv_spec.kind == "udt"
464
+ and recv_spec.name in DRAWING_TYPE_TO_CPP):
465
+ dtype = recv_spec.name
466
+ if dtype is None:
467
+ return False
468
+ if method in ("new", "copy"):
469
+ return False # constructors / copy return a handle
470
+ if (dtype, method) in _DRAWING_GETTER_RET:
471
+ return False # getters return a scalar
472
+ geometry, noop = DRAWING_METHODS_BY_TYPE.get(dtype, (frozenset(), frozenset()))
473
+ # any other recognised drawing method is a setter / delete / visual-noop
474
+ return method in geometry or method in noop
475
+
476
+ def _call_is_void(self, node) -> bool:
477
+ """True if ``node`` is a call that lowers to a VOID (or non-scalar) C++
478
+ expression and so cannot be used as a function's return value / RHS.
479
+
480
+ Covers drawing setters/delete/visual-noop (delegated to
481
+ ``_drawing_call_is_void``) AND the Pine array MUTATOR methods whose C++
482
+ lowering is void / an iterator (``array.push/insert/clear/set/fill/
483
+ sort/reverse/concat/unshift``) — a function ending in
484
+ ``array.clear(x)`` returns void in Pine, so it must emit as a statement
485
+ with a default return, not ``return x.clear();``.
486
+ """
487
+ if self._drawing_call_is_void(node):
488
+ return True
489
+ if not isinstance(node, FuncCall) or not isinstance(node.callee, MemberAccess):
490
+ return False
491
+ method = node.callee.member
492
+ _fn, ns = self._resolve_callee(node.callee)
493
+ if method not in _ARRAY_VOID_METHODS:
494
+ return False
495
+ # array.<method>(arr, ...) namespace form OR arr.<method>(...) method
496
+ # form on a std::vector receiver.
497
+ if ns == "array":
498
+ return True
499
+ recv_spec = self._type_spec_from_expr(node.callee.object)
500
+ return recv_spec is not None and recv_spec.kind == "array"
501
+
502
+ # ------------------------------------------------------------------
503
+ # _uses_drawing detection + arena caps
504
+ # ------------------------------------------------------------------
505
+ def _spec_mentions_drawing(self, spec) -> bool:
506
+ if spec is None:
507
+ return False
508
+ if spec.kind == "udt" and spec.name in DRAWING_TYPE_TO_CPP:
509
+ return True
510
+ if spec.kind == "array":
511
+ return self._spec_mentions_drawing(spec.element)
512
+ if spec.kind == "map":
513
+ return self._spec_mentions_drawing(spec.key) or self._spec_mentions_drawing(spec.value)
514
+ if spec.kind == "matrix":
515
+ return self._spec_mentions_drawing(spec.element)
516
+ return False
517
+
518
+ def _detect_drawing_usage(self) -> bool:
519
+ """True iff emitted C++ needs pineforge/drawing.hpp + the arenas.
520
+
521
+ Mirrors ``_detect_matrix_usage``: any var/field/array of a drawing type
522
+ OR any line/box/label/linefill namespace call OR any chart.point call.
523
+ """
524
+ for t in self._udt_var_types.values():
525
+ if t in DRAWING_TYPE_TO_CPP:
526
+ return True
527
+ for spec in self._collection_types.values():
528
+ if self._spec_mentions_drawing(spec):
529
+ return True
530
+ for fields in self._udt_field_type_specs.values():
531
+ for spec in fields.values():
532
+ if self._spec_mentions_drawing(spec):
533
+ return True
534
+ for _tname, _fields in self._udt_defs.items():
535
+ for f in _fields:
536
+ if self._spec_mentions_drawing(self._type_spec_from_hint_name(f.type_name)):
537
+ return True
538
+ from .tables import DRAWING_NS
539
+ for node in self._walk_ast(self.ctx.ast):
540
+ if isinstance(node, FuncCall):
541
+ _fn, ns = self._resolve_callee(node.callee)
542
+ if ns in DRAWING_NS:
543
+ return True
544
+ if self._is_chart_point_callee(node.callee):
545
+ return True
546
+ return False
547
+
548
+ def _compute_drawing_caps(self) -> dict:
549
+ """Per-type arena capacity from the strategy() header (default 50)."""
550
+ from ..ast_nodes import StrategyDecl
551
+ caps = {"line": 50, "box": 50, "label": 50, "linefill": 50}
552
+ header_field = {"line": "max_lines_count", "box": "max_boxes_count",
553
+ "label": "max_labels_count"}
554
+ for node in self._walk_ast(self.ctx.ast):
555
+ if isinstance(node, StrategyDecl):
556
+ for key, field in header_field.items():
557
+ v = self._int_literal_value(node.kwargs.get(field))
558
+ if v is not None and v > 0:
559
+ caps[key] = v
560
+ return caps