@pineforge/codegen-pyodide 1.1.0 → 1.2.0

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/LICENSE ADDED
@@ -0,0 +1,245 @@
1
+ PineForge Source License 1.1
2
+ ============================
3
+
4
+ Copyright 2025-2026 pineforge, LLC
5
+
6
+ Required Notice: Copyright 2025-2026 pineforge, LLC (https://github.com/pineforge-4pass/pineforge-codegen-oss)
7
+
8
+ The software is free of charge for the permitted purposes below, including
9
+ Personal Trading. Commercial Use, defined below, needs a separate commercial
10
+ license from the licensor.
11
+
12
+
13
+ ## Acceptance
14
+
15
+ In order to get any license under these terms, you must agree to them as both
16
+ strict obligations and conditions to all your licenses.
17
+
18
+
19
+ ## Copyright License
20
+
21
+ The licensor grants you a copyright license for the software to do everything
22
+ you might do with the software that would otherwise infringe the licensor's
23
+ copyright in it for any permitted purpose. However, you may only distribute
24
+ the software according to the Distribution License section, and make changes
25
+ or new works based on the software according to the Changes and New Works
26
+ License section.
27
+
28
+
29
+ ## Distribution License
30
+
31
+ The licensor grants you an additional copyright license to distribute copies
32
+ of the software. Your license to distribute covers distributing the software
33
+ with changes and new works permitted by the Changes and New Works License
34
+ section. Unless it is a permitted purpose, distributing the software, changed
35
+ or not, embedded in or bundled with a product or service made available to
36
+ others is Commercial Use under (3) of the Commercial Use section, which your
37
+ license to distribute does not cover. Distributing copies under this section
38
+ is not Commercial Use, and it gives the people who get them no license beyond
39
+ these terms.
40
+
41
+
42
+ ## Notices
43
+
44
+ You must ensure that anyone who gets a copy of any part of the software from
45
+ you also gets a copy of these terms or a link to them, as well as copies of
46
+ any plain-text lines beginning with `Required Notice:` that the licensor
47
+ provided with the software. For example:
48
+
49
+ > Required Notice: Copyright Yoyodyne, Inc. (http://example.com)
50
+
51
+
52
+ ## Changes and New Works License
53
+
54
+ The licensor grants you an additional copyright license to make changes and
55
+ new works based on the software for any permitted purpose.
56
+
57
+
58
+ ## Patent License
59
+
60
+ The licensor grants you a patent license for the software that covers patent
61
+ claims the licensor can license, or becomes able to license, that you would
62
+ infringe by using the software for a permitted purpose.
63
+
64
+
65
+ ## Permitted Purposes
66
+
67
+ Each of the following is a permitted purpose:
68
+
69
+ - **Noncommercial purposes.** Any noncommercial purpose.
70
+
71
+ - **Personal uses.** Personal use for research, experiment, and testing for
72
+ the benefit of public knowledge, personal study, private entertainment,
73
+ hobby projects, amateur pursuits, or religious observance, without any
74
+ anticipated commercial application.
75
+
76
+ - **Noncommercial organizations.** Use by any charitable organization,
77
+ educational institution, public research organization, public safety or
78
+ health organization, environmental protection organization, or government
79
+ institution for its teaching, research and other operations, regardless of
80
+ the source of funding or obligations resulting from the funding.
81
+
82
+ - **Personal Trading**, as defined in the next section.
83
+
84
+ Investment Management, defined in the Commercial Use section, is never a
85
+ permitted purpose, except as Personal Trading. That is so even when it is done
86
+ by a noncommercial organization or for a noncommercial purpose.
87
+
88
+
89
+ ## Personal Trading
90
+
91
+ **Personal Trading** means use of the software by a natural person to do any
92
+ of the following, solely for trades that meet conditions (a) and (b) below:
93
+ research, develop or backtest trading strategies, or execute trades in
94
+ securities, derivatives, currencies, crypto-assets, commodities or other
95
+ financial instruments.
96
+
97
+ (a) The trades are, or will be, placed for that person's own account.
98
+
99
+ (b) The trades are, or will be, funded solely by that person's own
100
+ capital.
101
+
102
+ For Personal Trading:
103
+
104
+ - A person's **household** is that person, their spouse or domestic
105
+ partner, and their dependants.
106
+
107
+ - A person's **own account** includes an account held by the person alone
108
+ or jointly with members of their household; an account of a member of
109
+ their household; and a retirement, pension, education-savings or other
110
+ tax-advantaged account, or an account of a revocable trust, held for the
111
+ benefit of the person or members of their household.
112
+
113
+ - A person's **own capital** includes capital belonging to members of
114
+ their household, and capital borrowed in the ordinary course from a
115
+ broker, bank or other lender, such as margin, leverage or credit on a
116
+ trading account, for which the person or their household is liable.
117
+ Money the person has received outright, such as pay, a gift or an
118
+ inheritance, is the person's own capital.
119
+
120
+ - Trading in the person's own name, for their own account and with their
121
+ own capital, is Personal Trading even when it is the person's business
122
+ or livelihood.
123
+
124
+ - Publishing or teaching about the person's own Personal Trading,
125
+ including its results, is part of Personal Trading even when the person
126
+ is paid for it, as long as they do not give Output to others.
127
+
128
+ - An account of a company, partnership, fund, trust or other
129
+ organization, other than an account described under own account above,
130
+ is not the person's own account, even if the person wholly owns or
131
+ controls it.
132
+
133
+ - Capital that an employer, a proprietary-trading firm, a funded-trader
134
+ program or anyone else outside the person's household provides or
135
+ allocates to the person to trade, and that the provider still owns or
136
+ whose losses the provider bears, is not the person's own capital, even
137
+ if the person shares in the profits.
138
+
139
+
140
+ ## Commercial Use
141
+
142
+ **Investment Management** means using the software to manage, advise on, or
143
+ trade investment capital; to research, develop or backtest strategies for
144
+ investment capital that you, or anyone you do the work for, manage, advise on
145
+ or trade; or to operate the software for others who do any of these things;
146
+ in each case whoever the capital belongs to and whether or not for a fee.
147
+
148
+ **Investment capital** means any money or other assets held for investment or
149
+ trading, including capital belonging to any other person or entity, such as a
150
+ friend or relative, clients or investors, an endowment, a pension or
151
+ retirement fund, a sovereign or other public fund, or the treasury or reserves
152
+ of a foundation or other organization.
153
+
154
+ **Commercial Use** means any use of the software that is not a permitted
155
+ purpose, other than distributing copies under the Distribution License
156
+ section. Investment Management is Commercial Use for every individual and
157
+ every organization, including the noncommercial organizations listed under
158
+ Permitted Purposes, whatever its purpose, unless it is Personal Trading.
159
+ Unless it is a permitted purpose, each of the following is also Commercial
160
+ Use:
161
+
162
+ (1) managing, advising on, or trading capital belonging to any other person
163
+ or entity, whether or not for a fee;
164
+
165
+ (2) use by, for, or on behalf of any company, fund, partnership, or other
166
+ organization, including use by an individual in the course of work for
167
+ such an organization;
168
+
169
+ (3) embedding the software or its Output, changed or not, into any product
170
+ or service made available to others, or distributing either bundled
171
+ with such a product or service, including using the software to
172
+ generate Output for such a product or service; and
173
+
174
+ (4) operating any hosted, software-as-a-service, or otherwise public-facing
175
+ service through which others can run the software or receive its
176
+ Output.
177
+
178
+ Commercial Use requires a separate commercial license from the licensor. To
179
+ obtain one, contact: enterprise@pineforge.dev
180
+
181
+
182
+ ## Output
183
+
184
+ **Output** means code that the software generates, such as the C++ source
185
+ code it generates from PineScript, and any program or library built from that
186
+ code. Backtest results, charts, reports and trade signals are not Output.
187
+
188
+
189
+ ## Fair Use
190
+
191
+ You may have "fair use" rights for the software under the law. These terms do
192
+ not limit them.
193
+
194
+
195
+ ## No Other Rights
196
+
197
+ These terms do not allow you to sublicense or transfer any of your licenses
198
+ to anyone else, or prevent the licensor from granting licenses to anyone
199
+ else. These terms do not imply any other licenses.
200
+
201
+
202
+ ## Patent Defense
203
+
204
+ If you make any written claim that the software infringes or contributes to
205
+ infringement of any patent, your patent license for the software granted
206
+ under these terms ends immediately. If your company makes such a claim, your
207
+ patent license ends immediately for work on behalf of your company.
208
+
209
+
210
+ ## Violations
211
+
212
+ The first time you are notified in writing that you have violated any of
213
+ these terms, or done anything with the software not covered by your licenses,
214
+ your licenses can nonetheless continue if you come into full compliance with
215
+ these terms, and take practical steps to correct past violations, within 32
216
+ days of receiving notice. Otherwise, all your licenses end immediately.
217
+
218
+
219
+ ## No Liability
220
+
221
+ ***As far as the law allows, the software comes as is, without any warranty
222
+ or condition, and the licensor will not be liable to you for any damages
223
+ arising out of these terms or the use or nature of the software, under any
224
+ kind of legal claim.***
225
+
226
+
227
+ ## Definitions
228
+
229
+ The **licensor** is pineforge, LLC, and the **software** is the software the
230
+ licensor makes available under these terms.
231
+
232
+ **You** refers to the individual or entity agreeing to these terms.
233
+
234
+ **Your company** is any legal entity, sole proprietorship, or other kind of
235
+ organization that you work for, plus all organizations that have control
236
+ over, are under the control of, or are under common control with that
237
+ organization. **Control** means ownership of substantially all the assets of
238
+ an entity, or the power to direct its management and policies by vote,
239
+ contract, or otherwise. Control can be direct or indirect.
240
+
241
+ **Your licenses** are all the licenses granted to you for the software under
242
+ these terms.
243
+
244
+ **Use** means anything you do with the software requiring one of your
245
+ licenses.
package/README.md CHANGED
@@ -16,6 +16,10 @@ Stable releases are on the `latest` dist-tag. A prerelease (for example
16
16
  - `transpile.worker.mjs` — an ES module worker that runs the glue in Pyodide.
17
17
  - `index.mjs` — exports `release`, `tables`, `archivePath`, `sourceRoot`, `codegenSourceDir`,
18
18
  `workerPath` and `glue`.
19
+ - `pineforge_codegen/diagnostics_catalog.json` — the diagnostics catalog (since 1.2.0), also
20
+ importable as `@pineforge/codegen-pyodide/diagnostics_catalog.json`; the repository's
21
+ `docs/PUBLIC_CONTRACT.md` describes it.
22
+ - `LICENSE` — the PineForge Source License 1.1 (since 1.2.0), which `package.json` names.
19
23
 
20
24
  ## Publishing (maintainers)
21
25
  A `v*` tag push, which the release workflow makes, publishes through npm OIDC Trusted
package/glue.py CHANGED
@@ -23,6 +23,10 @@ def _diagnostic_entries(diagnostics) -> list:
23
23
  "col": loc.col if loc else 1,
24
24
  "message": message,
25
25
  "severity": getattr(d.level, "value", "error"),
26
+ # The stable code and named arguments of the catalog templates the
27
+ # message (and the hint after " — ") render from.
28
+ "code": d.code,
29
+ "args": d.args,
26
30
  }
27
31
  end_col = getattr(loc, "end_col", None) if loc else None
28
32
  if end_col is not None:
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "@pineforge/codegen-pyodide",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Gate-validated Pyodide payload for the PineScript v6 -> C++ transpiler: archive (run in Pyodide), unpacked source, introspected tables, and release metadata.",
5
5
  "type": "module",
6
6
  "main": "index.mjs",
7
7
  "exports": {
8
8
  ".": "./index.mjs",
9
9
  "./tables.json": "./tables.json",
10
- "./release.json": "./release.json"
10
+ "./release.json": "./release.json",
11
+ "./diagnostics_catalog.json": "./pineforge_codegen/diagnostics_catalog.json"
11
12
  },
12
13
  "files": [
13
14
  "index.mjs",
@@ -16,7 +17,8 @@
16
17
  "tables.json",
17
18
  "release.json",
18
19
  "transpile.worker.mjs",
19
- "glue.py"
20
+ "glue.py",
21
+ "LICENSE"
20
22
  ],
21
23
  "engines": {
22
24
  "node": ">=20"
@@ -35,5 +37,5 @@
35
37
  "type": "git",
36
38
  "url": "git+https://github.com/pineforge-4pass/pineforge-codegen-oss.git"
37
39
  },
38
- "license": "PolyForm-Noncommercial-1.0.0"
40
+ "license": "SEE LICENSE IN LICENSE"
39
41
  }
@@ -6,6 +6,7 @@ from .lexer import Lexer
6
6
  from .parser import Parser
7
7
  from .analyzer import Analyzer
8
8
  from .codegen import CodeGen
9
+ from .diagnostic_codes import diagnostics_catalog, render_diagnostic
9
10
  from .errors import CompileError, Level, Phase
10
11
  from .external_requests import lower_no_data_requests
11
12
  from .builtin_keywords import bind_builtin_keywords
@@ -4414,7 +4414,44 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
4414
4414
  return None
4415
4415
 
4416
4416
  def generate(self) -> str:
4417
- """Generate C++ source from the AnalyzerContext."""
4417
+ """Generate C++ source from the AnalyzerContext.
4418
+
4419
+ An error the settings metadata finds in an input's arguments (emitted
4420
+ in the constructor, ahead of the body: ``checked_settings``) is held
4421
+ until the rest is generated; the error raised is the first in source
4422
+ order, as before that metadata visited the arguments."""
4423
+ self._deferred_settings_errors: list[CompileError] | None = []
4424
+ try:
4425
+ try:
4426
+ cpp = self._generate_cpp()
4427
+ except CompileError as error:
4428
+ first = self._first_settings_error_before(error)
4429
+ if first is None:
4430
+ raise
4431
+ raise first from None
4432
+ if self._deferred_settings_errors:
4433
+ raise self._deferred_settings_errors[0]
4434
+ return cpp
4435
+ finally:
4436
+ self._deferred_settings_errors = None
4437
+
4438
+ def _defer_settings_error(self, error: CompileError) -> None:
4439
+ held = getattr(self, "_deferred_settings_errors", None)
4440
+ if held is None: # outside generate(): nothing holds it
4441
+ raise error
4442
+ held.append(error)
4443
+
4444
+ def _first_settings_error_before(self, error: CompileError) -> CompileError | None:
4445
+ """A held settings error located before ``error``, else None."""
4446
+ def where(err: CompileError):
4447
+ loc = err.diagnostics[0].location if err.diagnostics else None
4448
+ return (loc.line, loc.col) if loc is not None else (float("inf"), 0)
4449
+ held = sorted(self._deferred_settings_errors, key=where)
4450
+ if held and where(held[0]) < where(error):
4451
+ return held[0]
4452
+ return None
4453
+
4454
+ def _generate_cpp(self) -> str:
4418
4455
  # Every input is keyed by its title: refuse a non-constant one first,
4419
4456
  # then flag inputs one override key would reach together.
4420
4457
  self._check_input_titles()
@@ -0,0 +1,186 @@
1
+ """Immutable execution metadata; it does not alter generated computation."""
2
+
3
+ import json
4
+ import math
5
+
6
+ from ..ast_nodes import BoolLiteral, FuncCall, Identifier, MemberAccess, NumberLiteral, StrategyDecl, StringLiteral, UnaryOp, VarDecl
7
+ from ..external_requests import CAPABILITY_UNPINNED_ANNOTATION
8
+ from ..limits import iter_ast_nodes
9
+
10
+
11
+ STRATEGY_PARAMETERS = (
12
+ "title", "shorttitle", "overlay", "format", "precision", "scale", "pyramiding",
13
+ "calc_on_order_fills", "calc_on_every_tick", "max_bars_back",
14
+ "backtest_fill_limits_assumption", "default_qty_type", "default_qty_value",
15
+ "initial_capital", "currency", "slippage", "commission_type", "commission_value",
16
+ "process_orders_on_close", "close_entries_rule", "margin_long", "margin_short",
17
+ "explicit_plot_zorder", "max_lines_count", "max_labels_count", "max_boxes_count",
18
+ "calc_bars_count", "risk_free_rate", "use_bar_magnifier", "fill_orders_on_standard_ohlc",
19
+ "max_polylines_count", "dynamic_requests", "behind_chart", "calc_on_every_history_tick",
20
+ )
21
+
22
+
23
+ DECLARATION_DEFAULTS = {
24
+ "calc_on_every_tick": False,
25
+ "calc_on_order_fills": False,
26
+ "process_orders_on_close": False,
27
+ "use_bar_magnifier": False,
28
+ "fill_orders_on_standard_ohlc": False,
29
+ "backtest_fill_limits_assumption": 0,
30
+ "currency": "currency.NONE",
31
+ "timeframe": "",
32
+ "timeframe_gaps": True,
33
+ "dynamic_requests": True,
34
+ "calc_on_every_history_tick": False,
35
+ }
36
+
37
+
38
+ def _expression(node):
39
+ if isinstance(node, (StringLiteral, NumberLiteral, BoolLiteral)):
40
+ return node.value
41
+ if isinstance(node, Identifier):
42
+ return node.name
43
+ if isinstance(node, MemberAccess):
44
+ return f"{_expression(node.object)}.{node.member}"
45
+ if isinstance(node, FuncCall):
46
+ return f"{_expression(node.callee)}({','.join(str(_expression(arg)) for arg in node.args)})"
47
+ return None
48
+
49
+
50
+ def _literal(node):
51
+ if isinstance(node, (StringLiteral, NumberLiteral, BoolLiteral)):
52
+ return node.value, True
53
+ if isinstance(node, UnaryOp) and node.op in ("+", "-"):
54
+ value, valid = _literal(node.operand)
55
+ if valid and type(value) in (int, float):
56
+ return value if node.op == "+" else -value, True
57
+ if isinstance(node, MemberAccess):
58
+ value = _expression(node)
59
+ if value and value.split(".")[0] in ("currency", "format", "scale", "strategy"):
60
+ return value, True
61
+ return _expression(node), False
62
+
63
+
64
+ def capabilities_document(emitter) -> str:
65
+ declarations = dict(DECLARATION_DEFAULTS)
66
+ unresolved = []
67
+ requests = []
68
+ lower_symbols = {}
69
+ intrabar = False
70
+ recorded = False
71
+ for node, _depth in iter_ast_nodes(emitter.ctx.ast):
72
+ if isinstance(node, StrategyDecl):
73
+ arguments = {STRATEGY_PARAMETERS[index] if index < len(STRATEGY_PARAMETERS)
74
+ else f"strategy() positional argument {index + 1}": argument
75
+ for index, argument in enumerate(node.args)}
76
+ unresolved.extend(arguments.keys() & node.kwargs.keys())
77
+ arguments.update(node.kwargs)
78
+ for name, argument in arguments.items():
79
+ value, valid = _literal(argument)
80
+ default = DECLARATION_DEFAULTS.get(name)
81
+ if name in DECLARATION_DEFAULTS:
82
+ valid &= type(value) is type(default)
83
+ if name == "backtest_fill_limits_assumption":
84
+ valid = (type(value) is int and value >= 0) or (
85
+ type(value) is float and math.isfinite(value) and value >= 0 and value == int(value))
86
+ if valid:
87
+ value = int(value)
88
+ declarations[name] = value
89
+ if not valid or name not in STRATEGY_PARAMETERS and name not in DECLARATION_DEFAULTS:
90
+ unresolved.append(name)
91
+ elif isinstance(node, VarDecl):
92
+ intrabar |= node.is_varip
93
+ elif isinstance(node, FuncCall):
94
+ name, namespace = emitter._resolve_callee(node.callee)
95
+ if namespace == "request" and name == "security_lower_tf":
96
+ expression = node.args[2] if len(node.args) > 2 else node.kwargs.get("expression")
97
+ lower_symbols[id(expression)] = node.args[0] if node.args else node.kwargs.get("symbol")
98
+ parts = (node.annotations or {}).get("pf_recorded_key")
99
+ if parts is not None:
100
+ recorded = True
101
+ requests.append({
102
+ "function": f"{namespace}.{name}",
103
+ "symbol": _expression(node.args[0]) if node.args else None,
104
+ "timeframe": "",
105
+ "lookahead": f"barmerge.lookahead_{parts.get('lookahead', 'off')}",
106
+ "gaps": f"barmerge.gaps_{parts.get('gaps', 'off')}",
107
+ "heikinashi": False,
108
+ "feed": "recorded",
109
+ })
110
+ if isinstance(node, Identifier) and node.name == "timenow":
111
+ unresolved.append("timenow (bar timestamp approximation)")
112
+ if isinstance(node, Identifier) and node.name in ("last_bar_index", "last_bar_time"):
113
+ unresolved.append(f"{node.name} (stream endpoint is not batch-equivalent)")
114
+ if isinstance(node, MemberAccess) and _expression(node) == "barstate.isrealtime":
115
+ unresolved.append("barstate.isrealtime (compiled historical-only approximation)")
116
+ if isinstance(node, MemberAccess) and _expression(node) in (
117
+ "barstate.islast", "barstate.islastconfirmedhistory"):
118
+ unresolved.append(f"{_expression(node)} (stream endpoint is not batch-equivalent)")
119
+
120
+ auxiliary = False
121
+ for site in emitter._security_calls:
122
+ symbol_node = site.get("symbol_node")
123
+ if site.get("is_lower_tf_array"):
124
+ symbol_node = lower_symbols.get(id(site.get("expr_node")), symbol_node)
125
+ if isinstance(symbol_node, FuncCall) and site.get("heikinashi") and symbol_node.args:
126
+ symbol_node = symbol_node.args[0]
127
+ symbol = _expression(symbol_node) if symbol_node is not None else "syminfo.tickerid"
128
+ timeframe = _expression(site.get("tf_node"))
129
+ lookahead = _expression(site.get("lookahead_node")) or "barmerge.lookahead_off"
130
+ gaps = _expression(site.get("gaps_node")) or "barmerge.gaps_off"
131
+ if isinstance(symbol_node, Identifier) and symbol_node.name not in emitter._global_mutable_infos:
132
+ symbol = _expression((getattr(emitter.ctx, "global_expr_map", {}) or {}).get(
133
+ symbol_node.name, symbol_node))
134
+ feed = "chart" if symbol in ("syminfo.tickerid", "syminfo.ticker", "") else "auxiliary"
135
+ auxiliary |= feed == "auxiliary"
136
+ if not isinstance(site.get("tf_node"), StringLiteral) and timeframe != "timeframe.period":
137
+ unresolved.append(f"request.security[{site['sec_id']}].timeframe")
138
+ if lookahead not in ("barmerge.lookahead_off", "barmerge.lookahead_on"):
139
+ unresolved.append(f"request.security[{site['sec_id']}].lookahead")
140
+ requests.append({
141
+ "function": "request.security_lower_tf" if site.get("is_lower_tf_array") else "request.security",
142
+ "symbol": symbol,
143
+ "timeframe": timeframe,
144
+ "lookahead": lookahead,
145
+ "gaps": gaps,
146
+ "heikinashi": bool(site.get("heikinashi")),
147
+ "feed": feed,
148
+ })
149
+ for index, site in enumerate((emitter.ctx.ast.annotations or {}).get(CAPABILITY_UNPINNED_ANNOTATION, ())):
150
+ requests.append({
151
+ "function": site["function"],
152
+ "symbol": _expression(site["symbol_node"]),
153
+ "timeframe": _expression(site["tf_node"]) if site["function"] in (
154
+ "request.security", "request.security_lower_tf") else "",
155
+ "lookahead": _expression(site["lookahead_node"]) or "barmerge.lookahead_off",
156
+ "gaps": _expression(site["gaps_node"]) or "barmerge.gaps_off",
157
+ "heikinashi": False,
158
+ "feed": "unpinned",
159
+ })
160
+ unresolved.append(f"{site['function']}[unpinned:{index}]")
161
+ requirements = {
162
+ "auxiliary_security_feeds": auxiliary,
163
+ "native_security_feeds": False,
164
+ "fx_curve": declarations["currency"] != "currency.NONE",
165
+ "recorded_series": recorded,
166
+ "historical_probe_overrides": False,
167
+ "intrabar_persistence": intrabar,
168
+ }
169
+ return json.dumps({"version": 1, "declarations": declarations, "requests": requests,
170
+ "requirements": requirements, "unresolved": sorted(set(unresolved))},
171
+ sort_keys=True, separators=(",", ":"), allow_nan=False)
172
+
173
+
174
+ def emit_capabilities_exports(emitter, lines: list[str]) -> None:
175
+ literal = json.dumps(capabilities_document(emitter))
176
+ lines.extend([
177
+ "#ifdef PF_SETTINGS_API_VERSION",
178
+ "#ifdef PF_CAPABILITIES_API_VERSION",
179
+ " uint32_t strategy_capabilities_api_version(void) { return PF_CAPABILITIES_API_VERSION; }",
180
+ " int strategy_capabilities_receipt(void* s, char* json, size_t capacity, size_t* required, char* error, size_t error_capacity) {",
181
+ " if (required) *required = 0;",
182
+ " return ::pineforge::checked_settings::boundary(error, error_capacity, [&] {",
183
+ ' ::pineforge::checked_settings::require(s != nullptr, "null strategy");',
184
+ f" ::pineforge::checked_settings::receipt({literal}, json, capacity, required);",
185
+ " });", " }", "#endif", "#endif",
186
+ ])
@@ -1,6 +1,19 @@
1
1
  """Opt-in settings metadata, validation and exception-contained C exports."""
2
2
 
3
3
  from ..ast_nodes import Identifier, MemberAccess
4
+ from ..errors import CompileError
5
+
6
+
7
+ def _visit_setting_arg(emitter, expr) -> str:
8
+ """An input argument's C++ for the settings metadata. The metadata is
9
+ emitted in the constructor, ahead of the script body: an error here is
10
+ held until the body is generated (``CodeGen.generate``), so the script's
11
+ first error in source order is the one raised."""
12
+ try:
13
+ return emitter._visit_expr(expr)
14
+ except CompileError as error:
15
+ emitter._defer_settings_error(error)
16
+ return "0"
4
17
 
5
18
 
6
19
  def emit_settings_members(emitter, lines: list[str], constructor: list[str]) -> None:
@@ -38,7 +51,7 @@ def emit_settings_members(emitter, lines: list[str], constructor: list[str]) ->
38
51
  key = emitter._input_key_literal(name)
39
52
  default = emitter._get_input_default(node)
40
53
  getter = emitter._input_getter_for_call(node, func_name, namespace)
41
- default_cpp = emitter._visit_expr(default) if default is not None else "0"
54
+ default_cpp = _visit_setting_arg(emitter, default) if default is not None else "0"
42
55
  default_cpp = emitter._coerce_string_input_default(getter, default_cpp)
43
56
  value_type = {
44
57
  "get_input_int": "int", "get_input_int64": "int",
@@ -89,7 +102,7 @@ def emit_settings_members(emitter, lines: list[str], constructor: list[str]) ->
89
102
  declared = getattr(arguments.get("options"), "elements", None)
90
103
  if declared:
91
104
  for option in declared:
92
- option_cpp = emitter._visit_expr(option)
105
+ option_cpp = _visit_setting_arg(emitter, option)
93
106
  options.append(option_cpp if getter == "get_input_string" else
94
107
  f'::pineforge::checked_settings::number({option_cpp})')
95
108
  expression = f'{getter}({key}, {default_cpp})'
@@ -98,7 +111,7 @@ def emit_settings_members(emitter, lines: list[str], constructor: list[str]) ->
98
111
  if default_serialized is None:
99
112
  default_serialized = (default_cpp if value_type in ("string", "source") else
100
113
  f'::pineforge::checked_settings::number({default_cpp})')
101
- constraints = [emitter._visit_expr(arguments[name]) if arguments.get(name) is not None
114
+ constraints = [_visit_setting_arg(emitter, arguments[name]) if arguments.get(name) is not None
102
115
  else "std::numeric_limits<double>::quiet_NaN()"
103
116
  for name in ("minval", "maxval", "step")]
104
117
  metadata = (f'{{{key}, "{value_type}", {default_serialized}, '
@@ -1998,6 +1998,8 @@ class TopLevelEmitter:
1998
1998
  lines.append(" }")
1999
1999
  from .checked_settings import emit_settings_exports
2000
2000
  emit_settings_exports(lines)
2001
+ from .capabilities import emit_capabilities_exports
2002
+ emit_capabilities_exports(self, lines)
2001
2003
  if self._declares_bar_magnifier():
2002
2004
  # TradingView runs a script that declares use_bar_magnifier = true
2003
2005
  # on its bar magnifier; the host reads this export to run it on
@@ -282,6 +282,7 @@ CPP_EMITTER_NAMES = frozenset("""
282
282
  configure_security_evaluators snapshot_script_state restore_script_state
283
283
  commit_script_state set_strategy_override set_input
284
284
  set_magnifier_volume_weighted strategy_declares_bar_magnifier fill_report run
285
+ strategy_capabilities_api_version strategy_capabilities_receipt
285
286
  precalculate
286
287
  strategy_entry strategy_close strategy_close_all strategy_exit
287
288
  strategy_exit_cancel_bracket strategy_cancel strategy_cancel_all strategy_order