@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.
Files changed (50) hide show
  1. package/README.md +16 -16
  2. package/glue.py +24 -16
  3. package/package.json +1 -1
  4. package/pineforge_codegen/__init__.py +125 -34
  5. package/pineforge_codegen/analyzer/__init__.py +2 -0
  6. package/pineforge_codegen/analyzer/base.py +754 -76
  7. package/pineforge_codegen/analyzer/call_handlers.py +260 -40
  8. package/pineforge_codegen/analyzer/contracts.py +37 -0
  9. package/pineforge_codegen/analyzer/diagnostics.py +30 -4
  10. package/pineforge_codegen/analyzer/tables.py +49 -8
  11. package/pineforge_codegen/analyzer/types.py +33 -1
  12. package/pineforge_codegen/ast_nodes.py +32 -1
  13. package/pineforge_codegen/block_locals.py +185 -0
  14. package/pineforge_codegen/builtin_keywords.py +42 -0
  15. package/pineforge_codegen/codegen/base.py +896 -156
  16. package/pineforge_codegen/codegen/constant_fold.py +131 -0
  17. package/pineforge_codegen/codegen/drawing.py +221 -79
  18. package/pineforge_codegen/codegen/emit_top.py +946 -213
  19. package/pineforge_codegen/codegen/helpers.py +435 -14
  20. package/pineforge_codegen/codegen/host_members.py +162 -0
  21. package/pineforge_codegen/codegen/input.py +252 -85
  22. package/pineforge_codegen/codegen/security.py +4372 -377
  23. package/pineforge_codegen/codegen/session_market.py +71 -0
  24. package/pineforge_codegen/codegen/ta.py +1188 -100
  25. package/pineforge_codegen/codegen/tables.py +193 -71
  26. package/pineforge_codegen/codegen/tv_number_format.py +270 -0
  27. package/pineforge_codegen/codegen/types.py +1882 -78
  28. package/pineforge_codegen/codegen/visit_call.py +920 -131
  29. package/pineforge_codegen/codegen/visit_expr.py +738 -57
  30. package/pineforge_codegen/codegen/visit_stmt.py +595 -49
  31. package/pineforge_codegen/external_requests.py +877 -0
  32. package/pineforge_codegen/lexer.py +104 -22
  33. package/pineforge_codegen/library_inline.py +1304 -0
  34. package/pineforge_codegen/library_modules.py +126 -0
  35. package/pineforge_codegen/library_v5.py +683 -0
  36. package/pineforge_codegen/limits.py +138 -0
  37. package/pineforge_codegen/method_binding.py +33 -0
  38. package/pineforge_codegen/parser.py +384 -68
  39. package/pineforge_codegen/pine_libraries.py +266 -0
  40. package/pineforge_codegen/pine_spelling.py +216 -0
  41. package/pineforge_codegen/pragmas.py +64 -10
  42. package/pineforge_codegen/security_contexts.py +1585 -0
  43. package/pineforge_codegen/session_reads.py +84 -0
  44. package/pineforge_codegen/signatures.py +48 -23
  45. package/pineforge_codegen/support_checker.py +1106 -85
  46. package/pineforge_codegen-1.0.0-rc.1.tar.gz +0 -0
  47. package/release.json +2 -2
  48. package/tables.json +23 -21
  49. package/transpile.worker.mjs +24 -16
  50. package/pineforge_codegen-0.10.4.tar.gz +0 -0
package/README.md CHANGED
@@ -1,25 +1,25 @@
1
1
  # @pineforge/codegen-pyodide
2
2
 
3
3
  Gate-validated Pyodide payload for the PineScript v6 → C++ transpiler. Built and
4
- published from `pineforge-codegen-oss` by `.github/workflows/publish-pyodide.yml`.
4
+ published from `pineforge-codegen-oss` by `.github/workflows/publish-pyodide.yml`,
5
+ with the same version as the matching `pineforge-codegen` release on PyPI.
6
+ Stable releases are on the `latest` dist-tag. A prerelease (for example
7
+ `1.0.0-rc.1`) is published on `next` only.
5
8
 
6
9
  ## Contents
7
10
  - `pineforge_codegen-<version>.tar.gz` — the gate-validated archive (unpack into Pyodide).
8
11
  - `pineforge_codegen/` — unpacked Python source (put on `PYTHONPATH` for Node oracle/grammar tooling).
9
- - `tables.json` — introspected codegen tables (the app renders `tables.generated.ts` from this).
12
+ - `tables.json` — introspected codegen tables (PineForge's web app renders its tables from this).
10
13
  - `release.json` — `{ codegen, pyodide, python, emscripten, sha256 }`.
11
- - `index.mjs` — `release`, `tables`, `archivePath`, `codegenSourceDir`.
14
+ - `glue.py` — the `transpile_json(source)` glue from the repository's `gate/glue.py`; its JSON
15
+ envelopes are described in the repository's `docs/PUBLIC_CONTRACT.md`.
16
+ - `transpile.worker.mjs` — an ES module worker that runs the glue in Pyodide.
17
+ - `index.mjs` — exports `release`, `tables`, `archivePath`, `sourceRoot`, `codegenSourceDir`,
18
+ `workerPath` and `glue`.
12
19
 
13
- ## One-time bootstrap (maintainer, manual)
14
- This is a NEW package; npm OIDC Trusted Publishing can only be configured after
15
- the package exists:
16
- 1. Build locally: `npm run gate:full && node scripts/build-npm-package.mjs`.
17
- 2. From `npm/`, do the first publish with a granular npm token:
18
- `npm publish --access=public` (one time).
19
- 3. On npmjs.com, configure the package's Trusted Publisher: GitHub Actions,
20
- repo `pineforge-4pass/pineforge-codegen-oss`, workflow `publish-pyodide.yml`.
21
- 4. Thereafter releases publish via OIDC (no token): run `publish-pyodide.yml`
22
- via **workflow_dispatch with `dry_run=false`**. NOTE: tag pushes currently run
23
- a DRY-RUN only (a safe validation) — the sole real-publish path is dispatch +
24
- `dry_run=false`. To switch to tag-driven releases later, change the publish
25
- condition in the workflow.
20
+ ## Publishing (maintainers)
21
+ A `v*` tag push, which the release workflow makes, publishes through npm OIDC Trusted
22
+ Publishing after the dependency audit and the full parity gate pass; published versions
23
+ carry npm provenance. A manual `workflow_dispatch` defaults to a dry run; `dry_run=false`
24
+ publishes for real. The one-time bootstrap is done: 0.7.0 was the manual first publish,
25
+ and every version since 0.7.1 carries provenance from this workflow.
package/glue.py CHANGED
@@ -13,23 +13,31 @@ from pineforge_codegen import transpile_full
13
13
  from pineforge_codegen.errors import CompileError
14
14
 
15
15
 
16
+ def _diagnostic_entries(diagnostics) -> list:
17
+ diags = []
18
+ for d in diagnostics:
19
+ loc = d.location
20
+ message = d.message + " — " + d.hint if getattr(d, "hint", None) else d.message
21
+ entry = {
22
+ "line": loc.line if loc else 1,
23
+ "col": loc.col if loc else 1,
24
+ "message": message,
25
+ "severity": getattr(d.level, "value", "error"),
26
+ }
27
+ end_col = getattr(loc, "end_col", None) if loc else None
28
+ if end_col is not None:
29
+ entry["endCol"] = end_col
30
+ diags.append(entry)
31
+ return diags
32
+
33
+
16
34
  def transpile_json(source: str) -> str:
17
35
  try:
18
36
  full = transpile_full(source)
19
37
  except CompileError as e:
20
- diags = []
21
- for d in e.diagnostics:
22
- loc = d.location
23
- message = d.message + " — " + d.hint if getattr(d, "hint", None) else d.message
24
- entry = {
25
- "line": loc.line if loc else 1,
26
- "col": loc.col if loc else 1,
27
- "message": message,
28
- "severity": getattr(d.level, "value", "error"),
29
- }
30
- end_col = getattr(loc, "end_col", None) if loc else None
31
- if end_col is not None:
32
- entry["endCol"] = end_col
33
- diags.append(entry)
34
- return json.dumps({"ok": False, "error": str(e), "diagnostics": diags})
35
- return json.dumps({"ok": True, "cpp": full["cpp"], "inputs": full["inputs"], "strategyParams": full["strategyParams"]})
38
+ return json.dumps({"ok": False, "error": str(e),
39
+ "diagnostics": _diagnostic_entries(e.diagnostics)})
40
+ # A script that transpiled carries its warnings in the same entry format.
41
+ return json.dumps({"ok": True, "cpp": full["cpp"], "inputs": full["inputs"],
42
+ "strategyParams": full["strategyParams"],
43
+ "diagnostics": _diagnostic_entries(full["diagnostics"])})
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pineforge/codegen-pyodide",
3
- "version": "0.10.4",
3
+ "version": "1.0.0-rc.1",
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",
@@ -1,15 +1,107 @@
1
1
  """PineScript v6 to C++ transpiler."""
2
2
 
3
+ from collections.abc import Mapping
4
+
3
5
  from .lexer import Lexer
4
6
  from .parser import Parser
5
7
  from .analyzer import Analyzer
6
8
  from .codegen import CodeGen
9
+ from .errors import CompileError, Level, Phase
10
+ from .external_requests import lower_no_data_requests
11
+ from .builtin_keywords import bind_builtin_keywords
7
12
  from .finite_ta_length import expand_finite_choice_extrema_lengths
13
+ from .library_inline import inline_libraries
14
+ from .limits import TimeBudget, check_ast_depth, check_source_size, ensure_recursion_headroom
8
15
  from .pragmas import extract_pf_trace_pragmas
16
+ from .security_contexts import specialize_security_contexts
17
+ from .block_locals import rename_block_locals
18
+ from .support_checker import check_support as _support_diagnostics
9
19
  from .support_checker import check_support_or_raise
10
20
 
11
21
 
12
- def transpile(pine_source: str, *, check_support: bool = True, filename: str = "<input>") -> str:
22
+ def _parse_bounded(pine_source: str, filename: str, budget: TimeBudget | None = None):
23
+ ensure_recursion_headroom()
24
+ check_source_size(pine_source, filename)
25
+ budget = budget or TimeBudget(filename)
26
+ pragmas = extract_pf_trace_pragmas(
27
+ pine_source, filename=filename, budget=budget,
28
+ )
29
+ budget.check(phase=Phase.LEXER)
30
+ tokens = Lexer(pine_source, filename=filename, budget=budget).tokenize()
31
+ budget.check(phase=Phase.LEXER)
32
+ ast = Parser(tokens, source=pine_source, filename=filename,
33
+ budget=budget).parse()
34
+ check_ast_depth(ast, filename)
35
+ budget.check(phase=Phase.PARSER)
36
+ return ast, pragmas, budget
37
+
38
+
39
+ def _generate(pine_source: str, check_support: bool, filename: str,
40
+ libraries: Mapping[str, str] | None = None):
41
+ """The pipeline, first without the per-call clones of functions that read
42
+ a ``session.<flag>[k]``, as scripts compiled before such reads were
43
+ supported; then again with the functions whose reads that C++ holds
44
+ cloned as well, until the C++ asks for no more (a clone can make a
45
+ caller's read reach the C++: its callers are cloned per call site, which
46
+ can retype their parameters). A read in an argument the codegen leaves
47
+ out never costs a clone (the clones of a function called along a deep
48
+ call tree grow with every path to it).
49
+
50
+ Each pass inlines the libraries the script imports before the support
51
+ check (``library_inline``). A declaration in a top-level block whose type
52
+ the member of its name cannot hold gets a name of its own, and the
53
+ pipeline runs again (``block_locals``).
54
+
55
+ Returns ``(codegen, ctx, cpp, support_diagnostics)``."""
56
+ budget = None
57
+ clones: frozenset[str] = frozenset()
58
+ renamed: frozenset = frozenset()
59
+ while True:
60
+ ast, pragmas, budget = _parse_bounded(pine_source, filename, budget)
61
+ ast = inline_libraries(ast, pine_source, libraries=libraries,
62
+ filename=filename, budget=budget)
63
+ rename_block_locals(ast, renamed)
64
+ support_diagnostics = []
65
+ if check_support:
66
+ support_diagnostics = _support_diagnostics(ast, filename=filename)
67
+ if any(d.level == Level.ERROR for d in support_diagnostics):
68
+ raise CompileError(support_diagnostics)
69
+ budget.check(phase=Phase.ANALYZER)
70
+ ast = lower_no_data_requests(ast)
71
+ ast = specialize_security_contexts(ast, filename=filename)
72
+ ast = bind_builtin_keywords(expand_finite_choice_extrema_lengths(ast))
73
+ check_ast_depth(ast, filename)
74
+ budget.check(phase=Phase.ANALYZER)
75
+ ctx = Analyzer(ast, filename=filename, budget=budget,
76
+ session_clones=clones).analyze()
77
+ budget.check(phase=Phase.ANALYZER)
78
+ # Attach after analysis: pragma expressions are not part of the
79
+ # program body, so the analyzer never inspects them; the codegen
80
+ # consumes them directly from the context to emit the on_bar tail
81
+ # ``if (trace_enabled_) { trace(...); ... }`` block.
82
+ ctx.pf_trace_pragmas = pragmas
83
+ gen = CodeGen(ctx, budget=budget)
84
+ cpp = gen.generate()
85
+ budget.check(phase=Phase.CODEGEN)
86
+ apart = gen.block_locals_needing_names - renamed
87
+ if apart:
88
+ renamed |= apart
89
+ del ast, ctx, gen, cpp
90
+ continue
91
+ needed = gen.session_functions_needing_clones
92
+ if not needed:
93
+ return gen, ctx, cpp, support_diagnostics
94
+ # Only uncloned functions ask, so the set grows each time and the
95
+ # loop ends by the time every function that reads a flag at an
96
+ # offset is cloned.
97
+ if needed <= clones:
98
+ raise AssertionError("a cloned function asked for session clones again")
99
+ clones |= needed
100
+ del ast, ctx, gen, cpp
101
+
102
+
103
+ def transpile(pine_source: str, *, check_support: bool = True, filename: str = "<input>",
104
+ libraries: Mapping[str, str] | None = None) -> str:
13
105
  """Transpile PineScript v6 source code to C++ code.
14
106
 
15
107
  Semantic rules enforced in :class:`Analyzer` (before codegen), including:
@@ -17,10 +109,10 @@ def transpile(pine_source: str, *, check_support: bool = True, filename: str = "
17
109
 
18
110
  Before analysis, :func:`support_checker.check_support_or_raise` rejects
19
111
  scripts that use language constructs PineForge cannot faithfully execute
20
- (e.g. ``indicator()`` declarations, prohibited variables such as
21
- ``bar_index``, disallowed ``request.security`` parameters). Pass
22
- ``check_support=False`` to bypass this gate (intended for tests of legacy
23
- fixtures only).
112
+ (e.g. ``indicator()`` declarations, ``request.seed``, disallowed
113
+ ``request.security`` parameters) and warns about approximated ones (e.g.
114
+ ``bar_index``). Pass ``check_support=False`` to bypass this gate
115
+ (experimental; intended for tests of legacy fixtures only).
24
116
 
25
117
  ``// @pf-trace name=expr`` pragmas are extracted from ``pine_source``
26
118
  via a pre-pass (the lexer strips comments before the parser sees them)
@@ -36,60 +128,59 @@ def transpile(pine_source: str, *, check_support: bool = True, filename: str = "
36
128
  ``file:line:col`` shown in ``CompileError`` (both ``str()`` and
37
129
  :meth:`~pineforge_codegen.errors.CompileError.format`) points back
38
130
  at the caller's file. Defaults to ``"<input>"``.
131
+ libraries: The sources of the Pine libraries the script imports,
132
+ by import path (``{"user/name/version": source_text}``); each
133
+ import the script uses is inlined from its source (see
134
+ :mod:`pineforge_codegen.library_inline`). ``None`` (default)
135
+ reads them from ``$PINEFORGE_PINE_LIBRARIES`` through the
136
+ script's own requests manifest under ``$PINEFORGE_REQUESTS_ROOT``
137
+ (see :mod:`pineforge_codegen.pine_libraries`); with neither, an
138
+ import is refused as before.
39
139
 
40
140
  Returns:
41
141
  Generated C++ source string.
42
142
  """
43
- pragmas = extract_pf_trace_pragmas(pine_source)
44
- tokens = Lexer(pine_source, filename=filename).tokenize()
45
- ast = Parser(tokens, source=pine_source, filename=filename).parse()
46
- if check_support:
47
- check_support_or_raise(ast, filename=filename)
48
- ast = expand_finite_choice_extrema_lengths(ast)
49
- ctx = Analyzer(ast, filename=filename).analyze()
50
- # Attach after analysis: pragma expressions are not part of the
51
- # program body, so the analyzer never inspects them; the codegen
52
- # consumes them directly from the context to emit the on_bar tail
53
- # ``if (trace_enabled_) { trace(...); ... }`` block.
54
- ctx.pf_trace_pragmas = pragmas
55
- return CodeGen(ctx).generate()
143
+ _gen, _ctx, cpp, _support = _generate(pine_source, check_support, filename, libraries)
144
+ return cpp
56
145
 
57
146
 
58
147
  def transpile_full(pine_source: str, *, check_support: bool = True,
59
- filename: str = "<input>") -> dict:
148
+ filename: str = "<input>",
149
+ libraries: Mapping[str, str] | None = None) -> dict:
60
150
  """Transpile like :func:`transpile`, plus the host-UI input manifest.
61
151
 
62
- Runs ONE pipeline pass (Lexer -> Parser -> support check -> Analyzer ->
63
- CodeGen.generate) and returns the generated C++ alongside the data the
152
+ Runs the pipeline (Lexer -> Parser -> support check -> Analyzer ->
153
+ CodeGen.generate) once, again while the C++ holds a
154
+ ``session.<flag>[k]`` read of a function not yet cloned for it (see
155
+ ``_generate``), and returns the generated C++ alongside the data the
64
156
  cloud Studio needs to auto-build a backtest "override params" form:
65
157
 
66
158
  - ``cpp``: the generated C++ source (identical to :func:`transpile`).
67
- - ``inputs``: a list of ``InputDef`` dicts (one per top-level
68
- ``var = input.*(...)`` declaration). Each has ``title`` / ``type`` /
159
+ - ``inputs``: a list of ``InputDef`` dicts (one per global-scope
160
+ ``input(...)`` / ``input.*(...)`` call, inline calls included, in
161
+ source order). Each has ``title`` / ``type`` /
69
162
  ``default`` and optionally ``min`` / ``max`` / ``step`` / ``options``
70
163
  (omitted when the corresponding signature argument is absent or
71
164
  references a non-const value). See
72
165
  :meth:`CodeGen.extract_input_manifest`.
73
166
  - ``strategyParams``: the literal ``strategy(...)`` kwargs the analyzer
74
167
  surfaced (e.g. ``initial_capital``, ``pyramiding``).
168
+ - ``diagnostics``: the warnings (:class:`~pineforge_codegen.errors.Diagnostic`,
169
+ ``Level.WARNING``) the support checker and the analyzer raised for a
170
+ script that transpiled -- e.g. an approximated ``ta.vwap`` anchor. An
171
+ error still raises ``CompileError``, which carries the warnings too.
75
172
 
76
173
  Args mirror :func:`transpile`.
77
174
 
78
175
  Returns:
79
- ``{"cpp": str, "inputs": list[dict], "strategyParams": dict}``.
176
+ ``{"cpp": str, "inputs": list[dict], "strategyParams": dict,
177
+ "diagnostics": list[Diagnostic]}``.
80
178
  """
81
- pragmas = extract_pf_trace_pragmas(pine_source)
82
- tokens = Lexer(pine_source, filename=filename).tokenize()
83
- ast = Parser(tokens, source=pine_source, filename=filename).parse()
84
- if check_support:
85
- check_support_or_raise(ast, filename=filename)
86
- ast = expand_finite_choice_extrema_lengths(ast)
87
- ctx = Analyzer(ast, filename=filename).analyze()
88
- ctx.pf_trace_pragmas = pragmas
89
- gen = CodeGen(ctx)
90
- cpp = gen.generate()
179
+ gen, ctx, cpp, support_diagnostics = _generate(pine_source, check_support, filename, libraries)
91
180
  return {
92
181
  "cpp": cpp,
93
182
  "inputs": gen.extract_input_manifest(),
94
183
  "strategyParams": dict(ctx.strategy_params),
184
+ "diagnostics": [d for d in (*support_diagnostics, *ctx.diagnostics)
185
+ if d.level == Level.WARNING],
95
186
  }
@@ -36,6 +36,7 @@ from .tables import (
36
36
  TA_TUPLE_RETURNS,
37
37
  TA_MULTI_CTOR,
38
38
  TA_NO_CTOR,
39
+ TA_LENGTH_ONLY_DEFAULT_SOURCE,
39
40
  BUILTIN_VARS,
40
41
  BAR_FIELDS,
41
42
  SKIP_FUNCS,
@@ -54,6 +55,7 @@ __all__ = [
54
55
  "TA_TUPLE_RETURNS",
55
56
  "TA_MULTI_CTOR",
56
57
  "TA_NO_CTOR",
58
+ "TA_LENGTH_ONLY_DEFAULT_SOURCE",
57
59
  "BUILTIN_VARS",
58
60
  "BAR_FIELDS",
59
61
  "SKIP_FUNCS",