@pineforge/codegen-pyodide 0.10.4 → 1.0.0-rc.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.
- package/README.md +16 -16
- package/glue.py +24 -16
- package/package.json +1 -1
- package/pineforge_codegen/__init__.py +125 -34
- package/pineforge_codegen/analyzer/__init__.py +2 -0
- package/pineforge_codegen/analyzer/base.py +754 -76
- package/pineforge_codegen/analyzer/call_handlers.py +260 -40
- package/pineforge_codegen/analyzer/contracts.py +37 -0
- package/pineforge_codegen/analyzer/diagnostics.py +30 -4
- package/pineforge_codegen/analyzer/tables.py +49 -8
- package/pineforge_codegen/analyzer/types.py +33 -1
- package/pineforge_codegen/ast_nodes.py +32 -1
- package/pineforge_codegen/block_locals.py +185 -0
- package/pineforge_codegen/builtin_keywords.py +42 -0
- package/pineforge_codegen/codegen/base.py +896 -156
- package/pineforge_codegen/codegen/constant_fold.py +131 -0
- package/pineforge_codegen/codegen/drawing.py +221 -79
- package/pineforge_codegen/codegen/emit_top.py +946 -213
- package/pineforge_codegen/codegen/helpers.py +435 -14
- package/pineforge_codegen/codegen/host_members.py +162 -0
- package/pineforge_codegen/codegen/input.py +252 -85
- package/pineforge_codegen/codegen/security.py +4372 -377
- package/pineforge_codegen/codegen/session_market.py +71 -0
- package/pineforge_codegen/codegen/ta.py +1188 -100
- package/pineforge_codegen/codegen/tables.py +193 -71
- package/pineforge_codegen/codegen/tv_number_format.py +270 -0
- package/pineforge_codegen/codegen/types.py +1882 -78
- package/pineforge_codegen/codegen/visit_call.py +920 -131
- package/pineforge_codegen/codegen/visit_expr.py +738 -57
- package/pineforge_codegen/codegen/visit_stmt.py +595 -49
- package/pineforge_codegen/external_requests.py +877 -0
- package/pineforge_codegen/lexer.py +104 -22
- package/pineforge_codegen/library_inline.py +1304 -0
- package/pineforge_codegen/library_modules.py +126 -0
- package/pineforge_codegen/library_v5.py +683 -0
- package/pineforge_codegen/limits.py +138 -0
- package/pineforge_codegen/method_binding.py +33 -0
- package/pineforge_codegen/parser.py +384 -68
- package/pineforge_codegen/pine_libraries.py +266 -0
- package/pineforge_codegen/pine_spelling.py +216 -0
- package/pineforge_codegen/pragmas.py +64 -10
- package/pineforge_codegen/security_contexts.py +1585 -0
- package/pineforge_codegen/session_reads.py +84 -0
- package/pineforge_codegen/signatures.py +48 -23
- package/pineforge_codegen/support_checker.py +1106 -85
- package/pineforge_codegen-1.0.0-rc.1.tar.gz +0 -0
- package/release.json +2 -2
- package/tables.json +23 -21
- package/transpile.worker.mjs +24 -16
- package/pineforge_codegen-0.10.4.tar.gz +0 -0
|
@@ -11,11 +11,14 @@ Mixin contract — host class must provide:
|
|
|
11
11
|
- ``self._resolve_callee`` (``NamingHelper``).
|
|
12
12
|
- ``self._visit_expr`` (visitor mixin, currently on ``base.py``;
|
|
13
13
|
used to render the title-arg fallback expression).
|
|
14
|
+
- ``self._cpp_string_escape`` (``NamingHelper``).
|
|
14
15
|
"""
|
|
15
16
|
|
|
16
17
|
from __future__ import annotations
|
|
17
18
|
|
|
18
19
|
from ..ast_nodes import (
|
|
20
|
+
ASTNode,
|
|
21
|
+
BinOp,
|
|
19
22
|
BoolLiteral,
|
|
20
23
|
FuncCall,
|
|
21
24
|
Identifier,
|
|
@@ -25,6 +28,7 @@ from ..ast_nodes import (
|
|
|
25
28
|
VarDecl,
|
|
26
29
|
)
|
|
27
30
|
from .. import signatures as sigs
|
|
31
|
+
from ..pine_spelling import expr_start, global_input_calls, input_binding_names
|
|
28
32
|
|
|
29
33
|
|
|
30
34
|
class InputHelper:
|
|
@@ -89,14 +93,23 @@ class InputHelper:
|
|
|
89
93
|
return node.kwargs["defval"]
|
|
90
94
|
return None
|
|
91
95
|
|
|
92
|
-
def
|
|
93
|
-
"""
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
96
|
+
def _input_default_value(self, node: FuncCall) -> tuple[bool, object]:
|
|
97
|
+
"""``(True, value)`` when the defval is the constant a ``v = input...``
|
|
98
|
+
binding records as ``v``'s known value -- a number, bool or string
|
|
99
|
+
literal, or a declared enum member (its index) -- else ``(False, None)``.
|
|
100
|
+
Only such an input is a stable scalar the TA reset can re-read."""
|
|
101
|
+
default = self._get_input_default(node)
|
|
102
|
+
if isinstance(default, (NumberLiteral, BoolLiteral, StringLiteral)):
|
|
103
|
+
return True, default.value
|
|
104
|
+
if isinstance(default, MemberAccess) and isinstance(default.object, Identifier):
|
|
105
|
+
members = self._enum_defs.get(default.object.name)
|
|
106
|
+
if members is not None and default.member in members:
|
|
107
|
+
return True, members.index(default.member)
|
|
108
|
+
return False, None
|
|
109
|
+
|
|
110
|
+
def _input_title_node(self, node: FuncCall):
|
|
111
|
+
"""The title argument of an ``input(...)`` call -- positional arg #2 or
|
|
112
|
+
the ``title=`` kwarg, via the signatures registry -- or None."""
|
|
100
113
|
func_name, namespace = self._resolve_callee(node.callee)
|
|
101
114
|
param_names = None
|
|
102
115
|
if namespace == "input" and func_name in sigs.INPUT_FUNCTIONS:
|
|
@@ -112,12 +125,127 @@ class InputHelper:
|
|
|
112
125
|
if i >= len(merged) or merged[i] is None:
|
|
113
126
|
merged[i] = node.kwargs[pname]
|
|
114
127
|
if len(merged) > 1 and merged[1] is not None:
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
128
|
+
return merged[1]
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
def _input_binding_names(self) -> dict[int, str]:
|
|
132
|
+
"""``id(call) -> name`` for the global-scope input calls a declaration
|
|
133
|
+
names (``pine_spelling.input_binding_names``)."""
|
|
134
|
+
names = getattr(self, "_input_binding_names_cache", None)
|
|
135
|
+
if names is None:
|
|
136
|
+
names = input_binding_names(self.ctx.ast.body)
|
|
137
|
+
self._input_binding_names_cache = names
|
|
138
|
+
return names
|
|
139
|
+
|
|
140
|
+
def _get_input_title(self, node: FuncCall, var_name: str | None = None) -> str:
|
|
141
|
+
"""The key an ``input(...)`` call is read and listed by: its title's
|
|
142
|
+
string value, else the name of the declaration binding it, else
|
|
143
|
+
``var_name``, else "".
|
|
144
|
+
|
|
145
|
+
The binding name comes from the call node itself, so every getter of
|
|
146
|
+
one input -- the member, the TA reset, a request.security timeframe,
|
|
147
|
+
an alias's read (``b = a``) -- keys it like the manifest does, whatever
|
|
148
|
+
name the caller passes. A title that is not a compile-time string
|
|
149
|
+
constant is refused (``_input_title_value``)."""
|
|
150
|
+
title_node = self._input_title_node(node)
|
|
151
|
+
if title_node is not None:
|
|
152
|
+
title = self._input_title_value(title_node)
|
|
153
|
+
if title is None:
|
|
154
|
+
self._refuse_input_title(title_node)
|
|
155
|
+
return title
|
|
156
|
+
bound = self._input_binding_names().get(id(node))
|
|
157
|
+
if bound is not None:
|
|
158
|
+
return bound
|
|
119
159
|
return var_name if var_name else ""
|
|
120
160
|
|
|
161
|
+
def _input_title_value(self, node, _seen: frozenset = frozenset()) -> str | None:
|
|
162
|
+
"""A title expression's compile-time string value, or None. TradingView
|
|
163
|
+
takes ``title (const string)``: a string literal, a ``+`` of constant
|
|
164
|
+
strings, or a name bound once at global scope, never reassigned and not
|
|
165
|
+
``var``, to one (``ctx.global_expr_map``)."""
|
|
166
|
+
if isinstance(node, StringLiteral):
|
|
167
|
+
return node.value
|
|
168
|
+
if isinstance(node, BinOp) and node.op == "+":
|
|
169
|
+
left = self._input_title_value(node.left, _seen)
|
|
170
|
+
right = self._input_title_value(node.right, _seen)
|
|
171
|
+
return None if left is None or right is None else left + right
|
|
172
|
+
if (isinstance(node, Identifier) and node.name not in _seen
|
|
173
|
+
and not self._known_var_is_lexically_shadowed(node.name)):
|
|
174
|
+
value = (getattr(self.ctx, "global_expr_map", None) or {}).get(node.name)
|
|
175
|
+
if value is not None:
|
|
176
|
+
return self._input_title_value(value, _seen | {node.name})
|
|
177
|
+
return None
|
|
178
|
+
|
|
179
|
+
def _refuse_input_title(self, title_node) -> None:
|
|
180
|
+
spelled = f"'{title_node.name}' " if isinstance(title_node, Identifier) else ""
|
|
181
|
+
self._codegen_error(
|
|
182
|
+
expr_start(title_node),
|
|
183
|
+
f"input title {spelled}is not a constant string: TradingView declares "
|
|
184
|
+
"title (const string), and PineForge keys an input override by its title.",
|
|
185
|
+
hint='Use a string literal, or a name bound once at global scope to one '
|
|
186
|
+
'(T = "Length").',
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
def _check_input_titles(self) -> None:
|
|
190
|
+
"""Refuse the first input call anywhere in the script whose title is not
|
|
191
|
+
a compile-time string constant, before any getter keys it: its C++ text
|
|
192
|
+
used to become the key (``std::string("Len")``)."""
|
|
193
|
+
stack: list = [self.ctx.ast]
|
|
194
|
+
while stack:
|
|
195
|
+
node = stack.pop()
|
|
196
|
+
if isinstance(node, (list, tuple)):
|
|
197
|
+
stack.extend(reversed(node))
|
|
198
|
+
continue
|
|
199
|
+
if isinstance(node, dict):
|
|
200
|
+
stack.extend(reversed(list(node.values())))
|
|
201
|
+
continue
|
|
202
|
+
if not isinstance(node, ASTNode):
|
|
203
|
+
continue
|
|
204
|
+
if isinstance(node, FuncCall) and self._is_input_call(node):
|
|
205
|
+
title_node = self._input_title_node(node)
|
|
206
|
+
if title_node is not None and self._input_title_value(title_node) is None:
|
|
207
|
+
self._refuse_input_title(title_node)
|
|
208
|
+
stack.extend(reversed([v for k, v in vars(node).items()
|
|
209
|
+
if k not in ("loc", "annotations")]))
|
|
210
|
+
|
|
211
|
+
def _check_input_keys(self) -> None:
|
|
212
|
+
"""Warn about inputs of the script that share an override key.
|
|
213
|
+
|
|
214
|
+
An override reaches every input keyed by its title, else by the name of
|
|
215
|
+
the declaration holding the call (``_get_input_title``, the manifest's
|
|
216
|
+
``title``), so inputs that share one cannot be overridden apart: two
|
|
217
|
+
untitled calls in one declaration (``n = input.int(9) +
|
|
218
|
+
input.int(3)``), two outside any declaration (keyed ""), an untitled
|
|
219
|
+
call and another input titled with its name, or one title repeated
|
|
220
|
+
across input groups (TradingView tells them apart; PineForge's
|
|
221
|
+
override protocol does not). The script still transpiles unchanged --
|
|
222
|
+
at its defaults every input reads its own default -- and one warning
|
|
223
|
+
per shared key names every input that key reaches.
|
|
224
|
+
"""
|
|
225
|
+
calls_by_key: dict[str, list[FuncCall]] = {}
|
|
226
|
+
for node, name in self._global_input_calls_with_names():
|
|
227
|
+
calls_by_key.setdefault(self._get_input_title(node, var_name=name), []).append(node)
|
|
228
|
+
for key, calls in calls_by_key.items():
|
|
229
|
+
if len(calls) < 2:
|
|
230
|
+
continue
|
|
231
|
+
spots = [f"{loc.line}:{loc.col}" for loc in (expr_start(c).loc for c in calls)]
|
|
232
|
+
listed = f"{', '.join(spots[:-1])} and {spots[-1]}"
|
|
233
|
+
self._codegen_warning(
|
|
234
|
+
expr_start(calls[1]),
|
|
235
|
+
f"input key '{key}' is shared by the inputs at {listed}: PineForge "
|
|
236
|
+
"sets an input override by its title, else by the name of the "
|
|
237
|
+
"declaration holding it, so one override sets all of them.",
|
|
238
|
+
hint="Give each input its own title to override them apart.",
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
def _input_spelling_title(self, node: FuncCall) -> str | None:
|
|
242
|
+
"""The key an untitled call's re-spelling must carry as ``title=``:
|
|
243
|
+
its binding name. A spelled call is re-parsed into a new node, which
|
|
244
|
+
no declaration names."""
|
|
245
|
+
if self._input_title_node(node) is not None:
|
|
246
|
+
return None
|
|
247
|
+
return self._input_binding_names().get(id(node))
|
|
248
|
+
|
|
121
249
|
# Native price series accepted as an input.source defval. The analyzer
|
|
122
250
|
# hard-rejects anything else (see support_checker), so a defval reaching
|
|
123
251
|
# codegen is one of these or a user-series identifier.
|
|
@@ -202,16 +330,33 @@ class InputHelper:
|
|
|
202
330
|
if self._is_source_input(node):
|
|
203
331
|
default = self._get_input_default(node)
|
|
204
332
|
base = self._source_defval_to_base_series(default)
|
|
205
|
-
return f'get_input_source(
|
|
333
|
+
return f'get_input_source({self._input_key_literal(title)}, {base})[0]'
|
|
206
334
|
default = self._get_input_default(node)
|
|
207
335
|
default_cpp = self._visit_expr(default) if default is not None else "0"
|
|
336
|
+
getter = self._input_getter_for_call(node, func_name, namespace)
|
|
337
|
+
default_cpp = self._coerce_string_input_default(getter, default_cpp)
|
|
338
|
+
return f'{getter}({self._input_key_literal(title)}, {default_cpp})'
|
|
339
|
+
|
|
340
|
+
_GETTER_CPP_TYPE = {
|
|
341
|
+
"get_input_bool": "bool", "get_input_int": "int",
|
|
342
|
+
"get_input_string": "std::string", "get_input_double": "double",
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
def _input_getter_for_call(self, node: FuncCall, func_name: str | None,
|
|
346
|
+
namespace: str | None) -> str:
|
|
347
|
+
"""The scalar getter an ``input`` call reads through.
|
|
348
|
+
|
|
349
|
+
The generic ``input(...)`` overload is typed by its defval in Pine
|
|
350
|
+
v6: ``input("x")`` is a string input, ``input(3)`` an int one and
|
|
351
|
+
``input(true)`` a bool one (lab tv tape te_generic_input: its
|
|
352
|
+
``str.tostring(n / 2)`` reads "1.5" and its string input spells the
|
|
353
|
+
order comments). The static getter table cannot see the default, so
|
|
354
|
+
the getter follows the default's literal type here. This matters for
|
|
355
|
+
TA lengths too: ``input(15)`` must route to ``get_input_int`` so the
|
|
356
|
+
RMA/EMA ctor receives an int."""
|
|
208
357
|
getter = self._input_type_to_getter(func_name, namespace)
|
|
209
|
-
# The generic ``input(...)`` overload is typed by its defval in Pine
|
|
210
|
-
# v6 (an int default yields an int input). The static getter table
|
|
211
|
-
# cannot see the default, so infer the getter from the default's
|
|
212
|
-
# literal type here. This matters for TA lengths: ``input(15)`` must
|
|
213
|
-
# route to ``get_input_int`` so the RMA/EMA ctor receives an int.
|
|
214
358
|
if func_name == "input" and namespace is None:
|
|
359
|
+
default = self._get_input_default(node)
|
|
215
360
|
if isinstance(default, BoolLiteral):
|
|
216
361
|
getter = "get_input_bool"
|
|
217
362
|
elif isinstance(default, NumberLiteral):
|
|
@@ -223,8 +368,22 @@ class InputHelper:
|
|
|
223
368
|
getter = "get_input_double"
|
|
224
369
|
elif isinstance(default, StringLiteral):
|
|
225
370
|
getter = "get_input_string"
|
|
226
|
-
|
|
227
|
-
|
|
371
|
+
return getter
|
|
372
|
+
|
|
373
|
+
def _generic_input_cpp_type(self, node: FuncCall) -> str:
|
|
374
|
+
"""The C++ storage of a generic ``input(...)`` value: its getter's
|
|
375
|
+
type (``_input_getter_for_call``), a source input's double."""
|
|
376
|
+
if self._is_source_input(node):
|
|
377
|
+
return "double"
|
|
378
|
+
return self._GETTER_CPP_TYPE[self._input_getter_for_call(node, "input", None)]
|
|
379
|
+
|
|
380
|
+
def _input_key_literal(self, title: str) -> str:
|
|
381
|
+
"""``title`` as the C++ string literal the input getters key it by.
|
|
382
|
+
|
|
383
|
+
A title is arbitrary Pine string text; pasted verbatim, a quote in it
|
|
384
|
+
ends the literal early and a backslash escapes the next character, so
|
|
385
|
+
the TU no longer compiles."""
|
|
386
|
+
return f'"{self._cpp_string_escape(title)}"'
|
|
228
387
|
|
|
229
388
|
def _coerce_string_input_default(self, getter: str, default_cpp: str) -> str:
|
|
230
389
|
"""String getters take a ``std::string`` default. An unresolved default
|
|
@@ -310,75 +469,83 @@ class InputHelper:
|
|
|
310
469
|
return names, merged
|
|
311
470
|
|
|
312
471
|
def extract_input_manifest(self) -> list[dict]:
|
|
313
|
-
"""Walk
|
|
472
|
+
"""Walk the script's global-scope ``input.*()`` calls into an InputDef
|
|
473
|
+
list, one entry per call site in source order: a top-level
|
|
474
|
+
``var = input.*(...)`` declaration and an inline call inside an
|
|
475
|
+
expression (``ta.ema(close, input.int(9, "Fast"))``) alike.
|
|
314
476
|
|
|
315
477
|
Each entry: ``{title, type, default[, min, max, step, options]}``. The
|
|
316
478
|
optional keys are emitted only when the corresponding signature
|
|
317
479
|
argument is a const literal; a bound/option referencing a non-literal
|
|
318
|
-
is omitted (never crashes).
|
|
480
|
+
is omitted (never crashes). ``title`` is the key the emitted C++ reads
|
|
481
|
+
the input by: the title argument, else the name of the declaration
|
|
482
|
+
holding the call (``pine_spelling.input_binding_names``), else "".
|
|
319
483
|
"""
|
|
320
|
-
|
|
484
|
+
return [self._input_manifest_entry(node, name)
|
|
485
|
+
for node, name in self._global_input_calls_with_names()]
|
|
486
|
+
|
|
487
|
+
def _global_input_calls_with_names(self) -> list[tuple[FuncCall, str | None]]:
|
|
488
|
+
"""Every global-scope input call in source order, with the name of the
|
|
489
|
+
declaration holding it (None outside a declaration)."""
|
|
490
|
+
out = []
|
|
321
491
|
for stmt in self.ctx.ast.body:
|
|
322
|
-
|
|
323
|
-
isinstance(stmt, VarDecl)
|
|
324
|
-
and isinstance(stmt.value, FuncCall)
|
|
325
|
-
and self._is_input_call(stmt.value)
|
|
326
|
-
):
|
|
327
|
-
continue
|
|
328
|
-
node = stmt.value
|
|
329
|
-
func_name, namespace = self._resolve_callee(node.callee)
|
|
330
|
-
names, merged = self._merged_args(node, func_name, namespace)
|
|
331
|
-
title = self._get_input_title(node, var_name=stmt.name)
|
|
332
|
-
default_node = self._get_input_default(node)
|
|
333
|
-
default_val = (
|
|
334
|
-
self._literal_or_none(default_node)
|
|
335
|
-
if default_node is not None
|
|
336
|
-
else None
|
|
337
|
-
)
|
|
338
|
-
if namespace == "input":
|
|
339
|
-
form_type = self._FORM_TYPE.get(func_name, "string")
|
|
340
|
-
else:
|
|
341
|
-
# Plain ``input(...)``: Pine types the result by its defval.
|
|
342
|
-
# The codegen already emits the matching scalar getter, so the
|
|
343
|
-
# manifest must mirror that — infer from the resolved default's
|
|
344
|
-
# Python type. ``bool`` MUST be tested before ``int`` because
|
|
345
|
-
# ``isinstance(True, int)`` is True. A None/non-literal default
|
|
346
|
-
# falls back to "string".
|
|
347
|
-
if isinstance(default_val, bool):
|
|
348
|
-
form_type = "bool"
|
|
349
|
-
elif isinstance(default_val, int):
|
|
350
|
-
form_type = "int"
|
|
351
|
-
elif isinstance(default_val, float):
|
|
352
|
-
form_type = "float"
|
|
353
|
-
elif isinstance(default_val, str):
|
|
354
|
-
form_type = "string"
|
|
355
|
-
else:
|
|
356
|
-
form_type = "string"
|
|
357
|
-
entry: dict = {
|
|
358
|
-
"title": title,
|
|
359
|
-
"type": form_type,
|
|
360
|
-
"default": default_val,
|
|
361
|
-
}
|
|
362
|
-
# Pull min/max/step/options by signature param name; emit only
|
|
363
|
-
# const literals so the override form never references a runtime
|
|
364
|
-
# value it can't reproduce.
|
|
365
|
-
if names:
|
|
366
|
-
idx = {n: i for i, n in enumerate(names)}
|
|
367
|
-
for key, pname in (("min", "minval"), ("max", "maxval"), ("step", "step")):
|
|
368
|
-
i = idx.get(pname)
|
|
369
|
-
if i is not None and i < len(merged) and merged[i] is not None:
|
|
370
|
-
v = self._literal_or_none(merged[i])
|
|
371
|
-
# bool is an int subclass — exclude it from numeric bounds
|
|
372
|
-
if isinstance(v, (int, float)) and not isinstance(v, bool):
|
|
373
|
-
entry[key] = v
|
|
374
|
-
oi = idx.get("options")
|
|
375
|
-
if oi is not None and oi < len(merged) and merged[oi] is not None:
|
|
376
|
-
opts_node = merged[oi]
|
|
377
|
-
elems = getattr(opts_node, "elements", None)
|
|
378
|
-
if elems is not None:
|
|
379
|
-
vals = [self._literal_or_none(e) for e in elems]
|
|
380
|
-
# any non-const element -> omit the whole options list
|
|
381
|
-
if vals and all(isinstance(v, str) for v in vals):
|
|
382
|
-
entry["options"] = vals
|
|
383
|
-
out.append(entry)
|
|
492
|
+
for node in global_input_calls(stmt):
|
|
493
|
+
out.append((node, stmt.name if isinstance(stmt, VarDecl) else None))
|
|
384
494
|
return out
|
|
495
|
+
|
|
496
|
+
def _input_manifest_entry(self, node: FuncCall, var_name: str | None) -> dict:
|
|
497
|
+
func_name, namespace = self._resolve_callee(node.callee)
|
|
498
|
+
names, merged = self._merged_args(node, func_name, namespace)
|
|
499
|
+
title = self._get_input_title(node, var_name=var_name)
|
|
500
|
+
default_node = self._get_input_default(node)
|
|
501
|
+
default_val = (
|
|
502
|
+
self._literal_or_none(default_node)
|
|
503
|
+
if default_node is not None
|
|
504
|
+
else None
|
|
505
|
+
)
|
|
506
|
+
if namespace == "input":
|
|
507
|
+
form_type = self._FORM_TYPE.get(func_name, "string")
|
|
508
|
+
else:
|
|
509
|
+
# Plain ``input(...)``: Pine types the result by its defval.
|
|
510
|
+
# The codegen already emits the matching scalar getter, so the
|
|
511
|
+
# manifest must mirror that — infer from the resolved default's
|
|
512
|
+
# Python type. ``bool`` MUST be tested before ``int`` because
|
|
513
|
+
# ``isinstance(True, int)`` is True. A None/non-literal default
|
|
514
|
+
# falls back to "string".
|
|
515
|
+
if isinstance(default_val, bool):
|
|
516
|
+
form_type = "bool"
|
|
517
|
+
elif isinstance(default_val, int):
|
|
518
|
+
form_type = "int"
|
|
519
|
+
elif isinstance(default_val, float):
|
|
520
|
+
form_type = "float"
|
|
521
|
+
elif isinstance(default_val, str):
|
|
522
|
+
form_type = "string"
|
|
523
|
+
else:
|
|
524
|
+
form_type = "string"
|
|
525
|
+
entry: dict = {
|
|
526
|
+
"title": title,
|
|
527
|
+
"type": form_type,
|
|
528
|
+
"default": default_val,
|
|
529
|
+
}
|
|
530
|
+
# Pull min/max/step/options by signature param name; emit only
|
|
531
|
+
# const literals so the override form never references a runtime
|
|
532
|
+
# value it can't reproduce.
|
|
533
|
+
if names:
|
|
534
|
+
idx = {n: i for i, n in enumerate(names)}
|
|
535
|
+
for key, pname in (("min", "minval"), ("max", "maxval"), ("step", "step")):
|
|
536
|
+
i = idx.get(pname)
|
|
537
|
+
if i is not None and i < len(merged) and merged[i] is not None:
|
|
538
|
+
v = self._literal_or_none(merged[i])
|
|
539
|
+
# bool is an int subclass — exclude it from numeric bounds
|
|
540
|
+
if isinstance(v, (int, float)) and not isinstance(v, bool):
|
|
541
|
+
entry[key] = v
|
|
542
|
+
oi = idx.get("options")
|
|
543
|
+
if oi is not None and oi < len(merged) and merged[oi] is not None:
|
|
544
|
+
opts_node = merged[oi]
|
|
545
|
+
elems = getattr(opts_node, "elements", None)
|
|
546
|
+
if elems is not None:
|
|
547
|
+
vals = [self._literal_or_none(e) for e in elems]
|
|
548
|
+
# any non-const element -> omit the whole options list
|
|
549
|
+
if vals and all(isinstance(v, str) for v in vals):
|
|
550
|
+
entry["options"] = vals
|
|
551
|
+
return entry
|