@pineforge/codegen-pyodide 1.0.1 → 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:
@@ -40,4 +44,5 @@ def transpile_json(source: str) -> str:
40
44
  # A script that transpiled carries its warnings in the same entry format.
41
45
  return json.dumps({"ok": True, "cpp": full["cpp"], "inputs": full["inputs"],
42
46
  "strategyParams": full["strategyParams"],
43
- "diagnostics": _diagnostic_entries(full["diagnostics"])})
47
+ "diagnostics": _diagnostic_entries(full["diagnostics"]),
48
+ "requests": full["requests"]})
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "@pineforge/codegen-pyodide",
3
- "version": "1.0.1",
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
@@ -13,6 +14,7 @@ from .finite_ta_length import expand_finite_choice_extrema_lengths
13
14
  from .library_inline import inline_libraries
14
15
  from .limits import TimeBudget, check_ast_depth, check_source_size, ensure_recursion_headroom
15
16
  from .pragmas import extract_pf_trace_pragmas
17
+ from .request_discovery import discover_requests, request_sites
16
18
  from .security_contexts import specialize_security_contexts
17
19
  from .block_locals import rename_block_locals
18
20
  from .support_checker import check_support as _support_diagnostics
@@ -52,7 +54,9 @@ def _generate(pine_source: str, check_support: bool, filename: str,
52
54
  the member of its name cannot hold gets a name of its own, and the
53
55
  pipeline runs again (``block_locals``).
54
56
 
55
- Returns ``(codegen, ctx, cpp, support_diagnostics)``."""
57
+ Returns ``(codegen, ctx, cpp, support_diagnostics, sites)``: ``sites``
58
+ are the requests of another symbol's data the support checker lowered
59
+ (``request_discovery.request_sites``)."""
56
60
  budget = None
57
61
  clones: frozenset[str] = frozenset()
58
62
  renamed: frozenset = frozenset()
@@ -67,6 +71,7 @@ def _generate(pine_source: str, check_support: bool, filename: str,
67
71
  if any(d.level == Level.ERROR for d in support_diagnostics):
68
72
  raise CompileError(support_diagnostics)
69
73
  budget.check(phase=Phase.ANALYZER)
74
+ sites = request_sites(ast)
70
75
  ast = lower_no_data_requests(ast)
71
76
  ast = specialize_security_contexts(ast, filename=filename)
72
77
  ast = bind_builtin_keywords(expand_finite_choice_extrema_lengths(ast))
@@ -86,18 +91,18 @@ def _generate(pine_source: str, check_support: bool, filename: str,
86
91
  apart = gen.block_locals_needing_names - renamed
87
92
  if apart:
88
93
  renamed |= apart
89
- del ast, ctx, gen, cpp
94
+ del ast, ctx, gen, cpp, sites
90
95
  continue
91
96
  needed = gen.session_functions_needing_clones
92
97
  if not needed:
93
- return gen, ctx, cpp, support_diagnostics
98
+ return gen, ctx, cpp, support_diagnostics, sites
94
99
  # Only uncloned functions ask, so the set grows each time and the
95
100
  # loop ends by the time every function that reads a flag at an
96
101
  # offset is cloned.
97
102
  if needed <= clones:
98
103
  raise AssertionError("a cloned function asked for session clones again")
99
104
  clones |= needed
100
- del ast, ctx, gen, cpp
105
+ del ast, ctx, gen, cpp, sites
101
106
 
102
107
 
103
108
  def transpile(pine_source: str, *, check_support: bool = True, filename: str = "<input>",
@@ -140,7 +145,8 @@ def transpile(pine_source: str, *, check_support: bool = True, filename: str = "
140
145
  Returns:
141
146
  Generated C++ source string.
142
147
  """
143
- _gen, _ctx, cpp, _support = _generate(pine_source, check_support, filename, libraries)
148
+ _gen, _ctx, cpp, _support, _sites = _generate(pine_source, check_support, filename,
149
+ libraries)
144
150
  return cpp
145
151
 
146
152
 
@@ -161,26 +167,36 @@ def transpile_full(pine_source: str, *, check_support: bool = True,
161
167
  source order). Each has ``title`` / ``type`` /
162
168
  ``default`` and optionally ``min`` / ``max`` / ``step`` / ``options``
163
169
  (omitted when the corresponding signature argument is absent or
164
- references a non-const value). See
165
- :meth:`CodeGen.extract_input_manifest`.
170
+ references a non-const value); an ``input.symbol`` entry also has
171
+ ``kind: "symbol"``. See :meth:`CodeGen.extract_input_manifest`.
166
172
  - ``strategyParams``: the literal ``strategy(...)`` kwargs the analyzer
167
173
  surfaced (e.g. ``initial_capital``, ``pyramiding``).
168
174
  - ``diagnostics``: the warnings (:class:`~pineforge_codegen.errors.Diagnostic`,
169
175
  ``Level.WARNING``) the support checker and the analyzer raised for a
170
176
  script that transpiled -- e.g. an approximated ``ta.vwap`` anchor. An
171
177
  error still raises ``CompileError``, which carries the warnings too.
178
+ - ``requests``: every request site that reads another symbol's feed,
179
+ by line: its symbol and timeframe as registration computes them
180
+ before the first bar (``literal`` / ``input`` / ``computed`` /
181
+ ``unresolvable``; ``literal`` / ``chart`` / ``input`` / ``computed``),
182
+ ``lookahead``, ``gaps`` and ``ignore_invalid_symbol``. A site lowered
183
+ to ``na`` (its value reaches display sinks only) or in a helper
184
+ nothing reaches is not listed. See
185
+ :mod:`pineforge_codegen.request_discovery`.
172
186
 
173
187
  Args mirror :func:`transpile`.
174
188
 
175
189
  Returns:
176
190
  ``{"cpp": str, "inputs": list[dict], "strategyParams": dict,
177
- "diagnostics": list[Diagnostic]}``.
191
+ "diagnostics": list[Diagnostic], "requests": list[dict]}``.
178
192
  """
179
- gen, ctx, cpp, support_diagnostics = _generate(pine_source, check_support, filename, libraries)
193
+ gen, ctx, cpp, support_diagnostics, sites = _generate(
194
+ pine_source, check_support, filename, libraries)
180
195
  return {
181
196
  "cpp": cpp,
182
197
  "inputs": gen.extract_input_manifest(),
183
198
  "strategyParams": dict(ctx.strategy_params),
184
199
  "diagnostics": [d for d in (*support_diagnostics, *ctx.diagnostics)
185
200
  if d.level == Level.WARNING],
201
+ "requests": discover_requests(gen, ctx, sites),
186
202
  }
@@ -3797,6 +3797,7 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
3797
3797
  symbol=sec.symbol,
3798
3798
  foreign=sec.foreign,
3799
3799
  ignore_invalid=sec.ignore_invalid,
3800
+ loc=sec.loc,
3800
3801
  )
3801
3802
  new_calls.append(clone)
3802
3803
  next_sec_id += 1
@@ -876,6 +876,7 @@ class CallHandlers:
876
876
  dead=bool(notes.get(DEAD_ANNOTATION)),
877
877
  foreign=notes.get(LOWERING_ANNOTATION) == FEED_LOWERING,
878
878
  ignore_invalid=node.kwargs.get("ignore_invalid_symbol"),
879
+ loc=node.loc,
879
880
  ))
880
881
 
881
882
  return PineType.STRING if string_result else PineType.FLOAT
@@ -998,6 +999,7 @@ class CallHandlers:
998
999
  is_lower_tf_array=True,
999
1000
  context_resolved=context is not None,
1000
1001
  dead=bool(notes.get(DEAD_ANNOTATION)),
1002
+ loc=node.loc,
1001
1003
  ))
1002
1004
 
1003
1005
  # ``request.security_lower_tf`` returns an array; the value-level
@@ -196,6 +196,9 @@ class SecurityCallInfo:
196
196
  # ``symbol`` string and ``ignore_invalid_symbol`` (the node, or None).
197
197
  foreign: bool = False
198
198
  ignore_invalid: Any = None
199
+ # The request call's source location (``request_discovery`` names the
200
+ # site by its line).
201
+ loc: Any = None
199
202
 
200
203
 
201
204
  @dataclass
@@ -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()