pineforge-codegen 0.6.5__tar.gz

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 (85) hide show
  1. pineforge_codegen-0.6.5/.github/dependabot.yml +8 -0
  2. pineforge_codegen-0.6.5/.github/workflows/release.yml +123 -0
  3. pineforge_codegen-0.6.5/.gitignore +122 -0
  4. pineforge_codegen-0.6.5/CLAUDE.md +402 -0
  5. pineforge_codegen-0.6.5/LICENSE +197 -0
  6. pineforge_codegen-0.6.5/PKG-INFO +462 -0
  7. pineforge_codegen-0.6.5/README.md +243 -0
  8. pineforge_codegen-0.6.5/VERSION +1 -0
  9. pineforge_codegen-0.6.5/docs/codegen-coverage-gaps.md +78 -0
  10. pineforge_codegen-0.6.5/pineforge_codegen/__init__.py +53 -0
  11. pineforge_codegen-0.6.5/pineforge_codegen/analyzer/__init__.py +60 -0
  12. pineforge_codegen-0.6.5/pineforge_codegen/analyzer/base.py +1563 -0
  13. pineforge_codegen-0.6.5/pineforge_codegen/analyzer/call_handlers.py +895 -0
  14. pineforge_codegen-0.6.5/pineforge_codegen/analyzer/contracts.py +163 -0
  15. pineforge_codegen-0.6.5/pineforge_codegen/analyzer/diagnostics.py +118 -0
  16. pineforge_codegen-0.6.5/pineforge_codegen/analyzer/tables.py +204 -0
  17. pineforge_codegen-0.6.5/pineforge_codegen/analyzer/types.py +250 -0
  18. pineforge_codegen-0.6.5/pineforge_codegen/ast_nodes.py +293 -0
  19. pineforge_codegen-0.6.5/pineforge_codegen/codegen/__init__.py +78 -0
  20. pineforge_codegen-0.6.5/pineforge_codegen/codegen/base.py +1381 -0
  21. pineforge_codegen-0.6.5/pineforge_codegen/codegen/emit_top.py +875 -0
  22. pineforge_codegen-0.6.5/pineforge_codegen/codegen/helpers.py +163 -0
  23. pineforge_codegen-0.6.5/pineforge_codegen/codegen/helpers_syminfo.py +134 -0
  24. pineforge_codegen-0.6.5/pineforge_codegen/codegen/input.py +189 -0
  25. pineforge_codegen-0.6.5/pineforge_codegen/codegen/security.py +1564 -0
  26. pineforge_codegen-0.6.5/pineforge_codegen/codegen/ta.py +298 -0
  27. pineforge_codegen-0.6.5/pineforge_codegen/codegen/tables.py +613 -0
  28. pineforge_codegen-0.6.5/pineforge_codegen/codegen/types.py +573 -0
  29. pineforge_codegen-0.6.5/pineforge_codegen/codegen/visit_call.py +1305 -0
  30. pineforge_codegen-0.6.5/pineforge_codegen/codegen/visit_expr.py +701 -0
  31. pineforge_codegen-0.6.5/pineforge_codegen/codegen/visit_stmt.py +729 -0
  32. pineforge_codegen-0.6.5/pineforge_codegen/errors.py +98 -0
  33. pineforge_codegen-0.6.5/pineforge_codegen/lexer.py +531 -0
  34. pineforge_codegen-0.6.5/pineforge_codegen/parser.py +1198 -0
  35. pineforge_codegen-0.6.5/pineforge_codegen/pragmas.py +117 -0
  36. pineforge_codegen-0.6.5/pineforge_codegen/signatures.py +808 -0
  37. pineforge_codegen-0.6.5/pineforge_codegen/support_checker.py +1111 -0
  38. pineforge_codegen-0.6.5/pineforge_codegen/symbols.py +118 -0
  39. pineforge_codegen-0.6.5/pineforge_codegen/tokens.py +406 -0
  40. pineforge_codegen-0.6.5/pineforge_codegen/tv_input_choices.py +86 -0
  41. pineforge_codegen-0.6.5/pyproject.toml +55 -0
  42. pineforge_codegen-0.6.5/tests/__init__.py +0 -0
  43. pineforge_codegen-0.6.5/tests/_compile.py +176 -0
  44. pineforge_codegen-0.6.5/tests/golden/matrix_eigen_pca.cpp +299 -0
  45. pineforge_codegen-0.6.5/tests/test_analyzer.py +221 -0
  46. pineforge_codegen-0.6.5/tests/test_analyzer_matrix_inference.py +77 -0
  47. pineforge_codegen-0.6.5/tests/test_analyzer_ta_return_types.py +108 -0
  48. pineforge_codegen-0.6.5/tests/test_codegen_fallthrough_guards.py +137 -0
  49. pineforge_codegen-0.6.5/tests/test_codegen_golden.py +91 -0
  50. pineforge_codegen-0.6.5/tests/test_codegen_input_getters.py +207 -0
  51. pineforge_codegen-0.6.5/tests/test_codegen_matrix_typed.py +412 -0
  52. pineforge_codegen-0.6.5/tests/test_codegen_new.py +1215 -0
  53. pineforge_codegen-0.6.5/tests/test_compile_corpus.py +145 -0
  54. pineforge_codegen-0.6.5/tests/test_compile_smoke.py +613 -0
  55. pineforge_codegen-0.6.5/tests/test_errors.py +33 -0
  56. pineforge_codegen-0.6.5/tests/test_input_time_int64.py +39 -0
  57. pineforge_codegen-0.6.5/tests/test_int64_time_storage.py +56 -0
  58. pineforge_codegen-0.6.5/tests/test_lexer.py +57 -0
  59. pineforge_codegen-0.6.5/tests/test_official_surface.py +426 -0
  60. pineforge_codegen-0.6.5/tests/test_parser.py +342 -0
  61. pineforge_codegen-0.6.5/tests/test_security_tf_literal.py +99 -0
  62. pineforge_codegen-0.6.5/tests/test_signatures.py +1000 -0
  63. pineforge_codegen-0.6.5/tests/test_support_checker.py +645 -0
  64. pineforge_codegen-0.6.5/tests/test_support_checker_chart_visible_bar_time.py +18 -0
  65. pineforge_codegen-0.6.5/tests/test_support_checker_color_cast.py +17 -0
  66. pineforge_codegen-0.6.5/tests/test_support_checker_dividends_earnings.py +28 -0
  67. pineforge_codegen-0.6.5/tests/test_support_checker_footprint.py +26 -0
  68. pineforge_codegen-0.6.5/tests/test_support_checker_input_color.py +75 -0
  69. pineforge_codegen-0.6.5/tests/test_support_checker_input_source.py +73 -0
  70. pineforge_codegen-0.6.5/tests/test_support_checker_matrix.py +55 -0
  71. pineforge_codegen-0.6.5/tests/test_support_checker_security_adjustment.py +76 -0
  72. pineforge_codegen-0.6.5/tests/test_support_checker_timeframe_from_seconds.py +21 -0
  73. pineforge_codegen-0.6.5/tests/test_support_checker_varip.py +53 -0
  74. pineforge_codegen-0.6.5/tests/test_support_checker_volume_row.py +21 -0
  75. pineforge_codegen-0.6.5/tests/test_symbols.py +42 -0
  76. pineforge_codegen-0.6.5/tests/test_ta_official_surface.py +163 -0
  77. pineforge_codegen-0.6.5/tests/test_transpile_division.py +115 -0
  78. pineforge_codegen-0.6.5/tests/test_transpile_enum_order.py +29 -0
  79. pineforge_codegen-0.6.5/tests/test_transpile_pf_trace.py +219 -0
  80. pineforge_codegen-0.6.5/tests/test_transpile_tr_handle_na.py +124 -0
  81. pineforge_codegen-0.6.5/tests/test_transpiler_matrix_kwargs.py +17 -0
  82. pineforge_codegen-0.6.5/tests/test_typespec_matrix.py +17 -0
  83. pineforge_codegen-0.6.5/tests/test_udt_drawing_field_cleanup.py +103 -0
  84. pineforge_codegen-0.6.5/tests/test_unsupported_reporting.py +178 -0
  85. pineforge_codegen-0.6.5/tests/test_vwap_tuple_unpack.py +146 -0
@@ -0,0 +1,8 @@
1
+ version: 2
2
+ updates:
3
+ # Keep the SHA-pinned GitHub Actions in workflows up to date with
4
+ # reviewable PRs (security review: pinned actions need a bump path).
5
+ - package-ecosystem: "github-actions"
6
+ directory: "/"
7
+ schedule:
8
+ interval: "weekly"
@@ -0,0 +1,123 @@
1
+ name: Release
2
+
3
+ # Manual release: bump VERSION, build the sdist + wheel, publish to PyPI via
4
+ # Trusted Publishing (OIDC — no stored token), then commit + tag + create a
5
+ # GitHub Release.
6
+
7
+ on:
8
+ workflow_dispatch:
9
+ inputs:
10
+ bump:
11
+ description: "Semver bump applied to VERSION file."
12
+ required: true
13
+ type: choice
14
+ default: patch
15
+ options: [patch, minor, major]
16
+ override:
17
+ description: "Optional explicit version (overrides bump). Leave blank to auto-bump."
18
+ required: false
19
+ type: string
20
+ default: ""
21
+ dry_run:
22
+ description: "Skip PyPI publish + tag; only build + show the new version."
23
+ required: false
24
+ type: boolean
25
+ default: false
26
+
27
+ permissions:
28
+ contents: write # commit + tag + create release
29
+ id-token: write # PyPI Trusted Publishing (OIDC)
30
+
31
+ concurrency:
32
+ group: release
33
+ cancel-in-progress: false
34
+
35
+ jobs:
36
+ release:
37
+ runs-on: ubuntu-latest
38
+ env:
39
+ BUMP: ${{ inputs.bump }}
40
+ OVERRIDE: ${{ inputs.override }}
41
+ steps:
42
+ - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
43
+ with:
44
+ fetch-depth: 0
45
+ # PAT (RELEASE_PAT) needed if a branch ruleset on `main` blocks
46
+ # GITHUB_TOKEN pushes. Falls back to GITHUB_TOKEN if unset.
47
+ token: ${{ secrets.RELEASE_PAT || secrets.GITHUB_TOKEN }}
48
+
49
+ - name: Compute new version
50
+ id: ver
51
+ run: |
52
+ set -euo pipefail
53
+ cur=$(tr -d '[:space:]' < VERSION)
54
+ if [ -n "$OVERRIDE" ]; then
55
+ new="$OVERRIDE"
56
+ else
57
+ IFS='.' read -r maj min pat <<< "$cur"
58
+ case "$BUMP" in
59
+ patch) pat=$((pat+1)) ;;
60
+ minor) min=$((min+1)); pat=0 ;;
61
+ major) maj=$((maj+1)); min=0; pat=0 ;;
62
+ *) echo "::error::bad bump '$BUMP'"; exit 1 ;;
63
+ esac
64
+ new="${maj}.${min}.${pat}"
65
+ fi
66
+ if ! [[ "$new" =~ ^[0-9]+\.[0-9]+\.[0-9]+([-+].+)?$ ]]; then
67
+ echo "::error::computed '$new' not semver"; exit 1
68
+ fi
69
+ if [ "$new" = "$cur" ] && [ -z "$OVERRIDE" ]; then
70
+ echo "::error::version unchanged ($cur)"; exit 1
71
+ fi
72
+ echo "cur=$cur" >> "$GITHUB_OUTPUT"
73
+ echo "new=$new" >> "$GITHUB_OUTPUT"
74
+ echo "Release $cur -> $new"
75
+
76
+ - name: Write VERSION
77
+ env:
78
+ NEW_VERSION: ${{ steps.ver.outputs.new }}
79
+ run: printf '%s\n' "$NEW_VERSION" > VERSION
80
+
81
+ - name: Show diff
82
+ run: git --no-pager diff
83
+
84
+ - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
85
+ with:
86
+ python-version: "3.12"
87
+
88
+ - name: Build sdist + wheel
89
+ run: |
90
+ python -m pip install --upgrade build
91
+ python -m build
92
+ ls -l dist/
93
+
94
+ # Commit + tag before publishing so a tag always corresponds to a built
95
+ # artifact set; the GitHub Release (with files) comes after PyPI succeeds.
96
+ - name: Commit + tag + push
97
+ if: ${{ !inputs.dry_run }}
98
+ env:
99
+ NEW_VERSION: ${{ steps.ver.outputs.new }}
100
+ run: |
101
+ git config user.name "github-actions[bot]"
102
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
103
+ git add VERSION
104
+ git commit -m "release: v${NEW_VERSION}"
105
+ git tag "v${NEW_VERSION}"
106
+ git push origin HEAD --tags
107
+
108
+ - name: Publish to PyPI
109
+ if: ${{ !inputs.dry_run }}
110
+ uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # release/v1
111
+
112
+ - name: Create GitHub Release
113
+ if: ${{ !inputs.dry_run }}
114
+ env:
115
+ # Org disables write for the default Actions token; use the admin PAT.
116
+ GH_TOKEN: ${{ secrets.RELEASE_PAT }}
117
+ NEW_VERSION: ${{ steps.ver.outputs.new }}
118
+ run: |
119
+ gh release create "v${NEW_VERSION}" \
120
+ --title "v${NEW_VERSION}" \
121
+ --generate-notes \
122
+ --notes "Install: \`pip install pineforge-codegen==${NEW_VERSION}\`" \
123
+ dist/*
@@ -0,0 +1,122 @@
1
+ # ─── Secrets — never commit ────────────────────────────────────────────────
2
+ .env
3
+ .env.*
4
+ !.env.example
5
+ .envrc
6
+ .npmrc
7
+ *.pem
8
+ *.key
9
+ *.crt
10
+ *.p12
11
+ *.pfx
12
+ *.token
13
+ *.secret
14
+ secrets.*
15
+ .secrets/
16
+
17
+ # ─── Python ────────────────────────────────────────────────────────────────
18
+ __pycache__/
19
+ *.py[cod]
20
+ *.pyo
21
+ *.so
22
+ *.egg-info/
23
+ .eggs/
24
+ build/
25
+ dist/
26
+ *.whl
27
+ .pytest_cache/
28
+ .mypy_cache/
29
+ .ruff_cache/
30
+ .tox/
31
+ .nox/
32
+ .pyre/
33
+ .pytype/
34
+ .coverage
35
+ .coverage.*
36
+ htmlcov/
37
+ coverage.xml
38
+ .hypothesis/
39
+
40
+ # Virtualenvs
41
+ .venv/
42
+ venv/
43
+ env/
44
+ ENV/
45
+ __pypackages__/
46
+
47
+ # uv / pip / poetry
48
+ uv.lock
49
+ poetry.lock
50
+ pip-wheel-metadata/
51
+
52
+ # Cython artifacts (kept out — built inside Docker for the hosted image)
53
+ pineforge_codegen/**/*.c
54
+ pineforge_codegen/**/*.cpp
55
+
56
+ # ─── Node / TypeScript ─────────────────────────────────────────────────────
57
+ node_modules/
58
+ **/node_modules/
59
+ package-lock.json
60
+ **/package-lock.json
61
+ yarn.lock
62
+ pnpm-lock.yaml
63
+ npm-debug.log*
64
+ yarn-debug.log*
65
+ yarn-error.log*
66
+ .pnp.*
67
+ .yarn/
68
+ .npm/
69
+ .eslintcache
70
+ *.tsbuildinfo
71
+
72
+ # Build output
73
+ dist/
74
+ **/dist/
75
+ build/
76
+ **/build/
77
+ out/
78
+ **/out/
79
+ .turbo/
80
+ .next/
81
+
82
+ # ─── Editor / IDE ──────────────────────────────────────────────────────────
83
+ .vscode/
84
+ !.vscode/extensions.json
85
+ !.vscode/settings.shared.json
86
+ .idea/
87
+ *.swp
88
+ *.swo
89
+ *.swn
90
+ *~
91
+ .history/
92
+
93
+ # ─── OS junk ───────────────────────────────────────────────────────────────
94
+ .DS_Store
95
+ .DS_Store?
96
+ ._*
97
+ .Spotlight-V100
98
+ .Trashes
99
+ ehthumbs.db
100
+ Thumbs.db
101
+ desktop.ini
102
+
103
+ # ─── Logs / temp / scratch ─────────────────────────────────────────────────
104
+ *.log
105
+ logs/
106
+ tmp/
107
+ temp/
108
+ *.tmp
109
+ *.bak
110
+ *.orig
111
+ *.rej
112
+ .scratch/
113
+
114
+ # ─── Tooling caches (Claude Code, AI agents) ───────────────────────────────
115
+ .claude/
116
+ .cursor/
117
+ .aider*
118
+
119
+ # ─── Project-specific notes ────────────────────────────────────────────────
120
+ # cloud/mcp-local/ was extracted to a public repo:
121
+ # https://github.com/fullpass-4pass/pineforge-codegen-mcp
122
+ # Don't bring it back here.
@@ -0,0 +1,402 @@
1
+ # CLAUDE.md — pineforge-codegen
2
+
3
+ > Project memory for AI coding agents. Keep terse and concrete. When the
4
+ > codebase invariants below are wrong, the truth is the test suite —
5
+ > update this file alongside any change that would break one of these
6
+ > claims.
7
+
8
+ ## REQUIRED before claiming any change is done
9
+
10
+ Both of the following MUST pass before opening a PR, marking a task
11
+ complete, or telling the user the change is ready. Skipping either
12
+ because "it's just a small change" is how the once-broken paths pinned
13
+ in `test_regression_*` survived for so long.
14
+
15
+ ```bash
16
+ # 1. Full pytest suite WITH the engine env var. Without the env var
17
+ # the 237 compile-only tests cleanly skip, which means every
18
+ # codegen change goes unverified at the C++ level. The env var is
19
+ # NOT optional for change verification — it is only optional for a
20
+ # quick "did I break a unit test" sanity loop during development.
21
+ # CRITICAL: Always rebuild the sibling pineforge-engine first if any
22
+ # C++ headers or source changed!
23
+ export PINEFORGE_ENGINE_INCLUDE=../pineforge-engine/include
24
+ pytest
25
+
26
+ # Expected at HEAD: 944 passed, 1 skipped, 0 failed.
27
+ # 1 skip is the pre-existing test_parser.py:335.
28
+ ```
29
+
30
+ ```bash
31
+ # 2. Corpus regression sweep. (Subset of step 1 — running step 1 already
32
+ # runs this. Listed separately because it is the load-bearing
33
+ # invariant: every Pine v6 strategy in the engine's parity corpus
34
+ # must transpile + compile against the engine headers.)
35
+ pytest tests/test_compile_corpus.py
36
+
37
+ # Expected at HEAD: 206 passed in ~47 s
38
+ # (basic 9 + community 11 + validation 147 + 16 validation_* sub-buckets
39
+ # + parity-anomalies 2).
40
+ ```
41
+
42
+ If either check newly fails, fix it before doing anything else. Adding
43
+ a strategy to `KNOWN_TRANSPILE_FAILURES` / `KNOWN_COMPILE_FAILURES` to
44
+ silence a corpus failure is only acceptable when the failure represents
45
+ an intentional, documented support drop — and even then, only with a
46
+ matching one-line rationale next to the entry.
47
+
48
+ The bare `pytest` (no env var) command remains useful during
49
+ development for a sub-second feedback loop on the transpiler-only path,
50
+ but it is **not sufficient** to validate a change. Always finish with
51
+ the env-var run.
52
+
53
+ ## What this is
54
+
55
+ PineScript v6 → C++ transpiler that emits source linking against the
56
+ `pineforge-engine` runtime (`<pineforge/engine.hpp>`, `<pineforge/ta.hpp>`,
57
+ …). Public entry point is `pineforge_codegen.transpile(pine_source) -> str`.
58
+
59
+ This is the **source-available** half of the PineForge stack (PolyForm
60
+ Noncommercial — see `LICENSE`). The runtime half (`pineforge-engine`,
61
+ Apache-2.0) lives in a sibling repo and is typically checked out at
62
+ `../pineforge-engine`. Versions are aligned:
63
+ `pineforge-codegen 0.X.Y` requires `pineforge-engine` at the matching ABI
64
+ tag — see the version table in `README.md`.
65
+
66
+ ## Pipeline
67
+
68
+ `transpile()` runs five passes in this order; respect the order when
69
+ adding work:
70
+
71
+ 1. `pragmas.extract_pf_trace_pragmas` — pulls `// @pf-trace name=expr`
72
+ comments out before the lexer strips comments.
73
+ 2. `Lexer` → `Parser` → `Program` AST.
74
+ 3. `support_checker.check_support_or_raise` — rejects any Pine surface
75
+ PineForge cannot faithfully execute (see "Support contracts" below).
76
+ 4. `Analyzer.analyze()` → `AnalyzerContext` (type inference, scope
77
+ resolution, per-call-site TA bookkeeping, security registration).
78
+ 5. `CodeGen(ctx).generate()` → C++ source string.
79
+
80
+ Pragmas are reattached to `ctx.pf_trace_pragmas` between (4) and (5)
81
+ because the analyzer never inspects them; codegen emits the trace tail
82
+ in `on_bar`.
83
+
84
+ ## File map
85
+
86
+ ```
87
+ pineforge_codegen/
88
+ ├── lexer.py / tokens.py Token stream
89
+ ├── parser.py / ast_nodes.py Pine v6 AST
90
+ ├── pragmas.py // @pf-trace extraction
91
+ ├── signatures.py Pine v6 builtin signature registry
92
+ │ (TA / math / str / strategy / input /
93
+ │ map / built-ins) — single source of
94
+ │ truth for kwargs + return types.
95
+ ├── support_checker.py SUPPORTED_* whitelists + HARD_REJECT
96
+ │ tables; raises CompileError for any
97
+ │ construct codegen would otherwise
98
+ │ silently miscompile.
99
+ ├── symbols.py PineType / TypeSpec / Symbol / Scope
100
+ │ / SymbolTable
101
+ ├── tv_input_choices.py input.string options metadata
102
+ ├── errors.py CompileError + SourceLocation +
103
+ │ Diagnostic + Level / Phase
104
+ ├── analyzer/
105
+ │ ├── base.py (~1.4k loc) Analyzer class — workhorse.
106
+ │ ├── call_handlers.py Per-call-namespace lowering helpers.
107
+ │ ├── contracts.py AnalyzerContext + sub-info dataclasses.
108
+ │ ├── tables.py TA_CLASS_MAP, TA_PERIOD_ARG,
109
+ │ │ TA_TUPLE_RETURNS, TA_MULTI_CTOR,
110
+ │ │ TA_NO_CTOR, BUILTIN_VARS, BAR_FIELDS,
111
+ │ │ SKIP_FUNCS.
112
+ │ ├── types.py Type-inference utilities.
113
+ │ └── diagnostics.py Analyzer warning/error emission.
114
+ └── codegen/
115
+ ├── base.py (~1.2k loc) CodeGen class — class-member layout,
116
+ │ include set, _matrix_vars / _array_vars
117
+ │ / _map_vars detection.
118
+ ├── emit_top.py #include block, extern "C" wrappers,
119
+ │ strategy_create / run_backtest_full /
120
+ │ strategy_set_input layouts.
121
+ ├── visit_stmt.py Statement-level visitors (var decl,
122
+ │ assign, if/for/while/switch).
123
+ ├── visit_expr.py Expression-level visitors (literals,
124
+ │ operators, ternary, member access).
125
+ ├── visit_call.py (~1.1k loc) Function-call dispatch — by far the
126
+ │ most heavily-special-cased file.
127
+ ├── ta.py TA call-site allocation + compute()
128
+ │ vs recompute() emission.
129
+ ├── security.py (~1.5k loc) request.security / _lower_tf plumbing.
130
+ ├── tables.py (~500 loc) Static dispatch tables: BAR_BUILTINS,
131
+ │ TA_*, ARRAY_METHODS, MAP_METHODS,
132
+ │ MATRIX_METHODS, MATRIX_RETURNING_METHODS,
133
+ │ MATH_FUNC_MAP, STR_FUNC_MAP, …
134
+ ├── types.py Codegen-side type inference
135
+ │ (_infer_type returns a C++ type STRING
136
+ │ like "std::string" / "double" / "int" /
137
+ │ "PineMatrix"). Used to gate emission
138
+ │ choices that the analyzer's PineType
139
+ │ enum is too coarse for.
140
+ ├── input.py input.* / `input()` lowering.
141
+ └── helpers.py CPP_RESERVED + small text helpers.
142
+ tests/
143
+ ├── _compile.py Helper that runs `g++ -fsyntax-only`
144
+ │ against the engine headers; reads
145
+ │ PINEFORGE_ENGINE_INCLUDE +
146
+ │ PINEFORGE_EIGEN_INCLUDE + CXX env vars.
147
+ │ Cleanly skips when env is missing.
148
+ ├── test_compile_smoke.py Hand-picked Pine snippets that hit
149
+ │ every dispatch lane; +3 regression
150
+ │ tests for once-broken paths
151
+ │ (year(time), matrix-returning methods,
152
+ │ str.format double-wrap).
153
+ ├── test_compile_corpus.py Parametrized over every
154
+ │ corpus/<bucket>/<strategy>/strategy.pine
155
+ │ from a sibling pineforge-engine
156
+ │ checkout (206 strategies, ~47 s).
157
+ ├── test_official_surface.py Locks SUPPORTED_* and signatures.* to
158
+ │ the Pine v6 official inventory
159
+ │ (sourced from user-pinescript-docs MCP).
160
+ ├── test_ta_official_surface.py Same idea, ta.*-specific (predates
161
+ │ the cross-namespace file).
162
+ ├── test_support_checker.py Per-rule support-checker behaviour.
163
+ ├── test_codegen_new.py (~1k loc) Substring assertions on emitted C++.
164
+ ├── test_signatures.py Signature-registry unit tests.
165
+ └── test_{lexer,parser,analyzer,symbols,errors,…}.py
166
+ ```
167
+
168
+ ## Architectural invariants — do not break
169
+
170
+ 1. **Versioned in lockstep with the engine ABI.** Bumping
171
+ `pyproject.toml::version` requires a matching `pineforge-engine` tag
172
+ that exposes the C ABI shape we emit against. The transpiler does NOT
173
+ include any runtime artifact at install time — the consumer links
174
+ `libpineforge.a` themselves.
175
+ 2. **Pure-Python, zero runtime deps.** `pyproject.toml::dependencies = []`.
176
+ Do not introduce a runtime dependency. `dev` extras are pytest only.
177
+ 3. **Tests never invoke a C++ compiler by default.** The compile-only
178
+ test harness (`tests/_compile.py`) is opt-in via
179
+ `PINEFORGE_ENGINE_INCLUDE`. Without that env var, every compile test
180
+ skips with a message naming the missing knob. Don't make compile
181
+ testing mandatory in CI without first plumbing the engine into CI.
182
+ 4. `**SUPPORTED_*` whitelists must equal the Pine v6 official inventory**
183
+ (modulo the `KNOWN_*_OMISSIONS` exception sets in
184
+ `tests/test_official_surface.py`). Adding a Pine surface item to
185
+ codegen requires adding it to the OFFICIAL_* set in the test file
186
+ AND removing it from any KNOWN_*_OMISSIONS set; new omissions need a
187
+ one-line rationale right next to the constant.
188
+ 5. **Every corpus strategy must transpile + compile.**
189
+ `test_compile_corpus.py` parametrizes over all 206
190
+ `corpus/*/*/strategy.pine` files. Per the "REQUIRED before claiming
191
+ any change is done" block at the top of this file, this is a
192
+ mandatory check on every change — not just changes to `analyzer/` or
193
+ `codegen/` — because parser, lexer, support-checker, and signature
194
+ tweaks can also break corpus strategies via second-order effects.
195
+ If you intentionally drop a Pine construct, add the strategy to
196
+ `KNOWN_TRANSPILE_FAILURES` / `KNOWN_COMPILE_FAILURES` with a
197
+ one-line rationale.
198
+
199
+ ## Support contracts
200
+
201
+ `pineforge_codegen.support_checker` is the gate. Its job is to fail
202
+ loudly **before** codegen so users never get a silently miscompiled
203
+ strategy. The taxonomy:
204
+
205
+
206
+ | Bucket | What |
207
+ | -------------------------------- | -------------------------------------------------------------------- |
208
+ | `HARD_REJECT_FUNC` | Calls with no PineForge semantics at all (e.g. `request.financial`). |
209
+ | `HARD_REJECT_NAMESPACE` | Whole namespaces (e.g. `ticker.*`). |
210
+ | `DIVERGENT_VARS` (warning) | Built-in variables whose value diverges from TV (e.g. `bar_index`). |
211
+ | `BARSTATE_APPROX_VARS` (warning) | Barstate flags PineForge approximates in batch mode. |
212
+ | `STRATEGY_UNSUPPORTED_PARAMS` | Per-strategy.* call kwargs that codegen drops silently. |
213
+ | `NOT_YET_FUNC` | Implementable but currently no codegen — reject loudly. |
214
+ | `SUPPORTED_*` frozensets | Per-namespace whitelist of names codegen knows how to emit. |
215
+ | `varip` VarDecl check | `varip` declarations rejected outright — batch backtests have no realtime tick state. |
216
+ | TF literal validation | `request.security` / `request.security_lower_tf` `timeframe` string literals validated against Pine v6 format at parse time. |
217
+
218
+
219
+ When extending codegen with a new Pine builtin: add to the corresponding
220
+ `SUPPORTED_*` set (or hard-reject) in `support_checker.py`, register its
221
+ signature in `signatures.py`, then wire the dispatch in
222
+ `analyzer/call_handlers.py` and `codegen/visit_call.py` (or the
223
+ namespace-specific visitor module). The cross-namespace official-surface
224
+ test will fail at PR time if you forget any of these steps.
225
+
226
+ ## Known codegen quirks (read before changing)
227
+
228
+ These bit us once and are now pinned by `test_compile_smoke.py`'s
229
+ `test_regression_*` cases. Treat the regression tests as canaries — if
230
+ you delete or weaken the special case, the test will tell you.
231
+
232
+ 1. **`year(time)` / `month(time)` / `dayofmonth` / `dayofweek` /
233
+ `hour(time, tz)` / `minute` / `second` / `weekofyear` function-call
234
+ form** lowers to an inline lambda that mirrors the engine's
235
+ `BacktestEngine::_decompose_bar_time()`. The variable form uses
236
+ `_bar_year()` etc. via `BAR_BUILTINS` (still UTC-only — `_bar_*`
237
+ accessors call `gmtime_r` unconditionally; chart-tz wiring on the
238
+ variable side is a separate engine change). The function-call form
239
+ honors a tz argument: the two-arg form `hour(time, tz)` uses the
240
+ explicit tz; the one-arg form `hour(time)` defaults to
241
+ `syminfo_.timezone` (the engine's `SymInfo::timezone` field, "UTC"
242
+ by default). When the resolved tz is empty / "UTC" / "Etc/UTC" the
243
+ lambda takes the cheap `gmtime_r` fast path; otherwise it enters a
244
+ mutex-guarded `setenv("TZ", ...)` + `localtime_r` block that
245
+ mirrors `pine_tz::ScopedTimezone` (`src/timezone.cpp`, not exposed
246
+ via any public `<pineforge/...>` header). Harnesses validating
247
+ against TV exports for non-UTC symbols MUST set chart_tz to match
248
+ the chart's TZ at export time.
249
+ 2. **Matrix-returning methods** (`inv` / `pinv` / `transpose` / `copy` /
250
+ `submatrix` / `concat` / `diff` / `mult` / `pow` / `eigenvectors` /
251
+ `kron`) live in `MATRIX_RETURNING_METHODS` (`codegen/tables.py`).
252
+ Both `_register_global_aggregate_member_types` (codegen/base.py) and
253
+ `_visit_var_decl` (codegen/visit_stmt.py) consult this set to declare
254
+ the LHS as `PineMatrix` instead of the analyzer's default `double`.
255
+ Methods returning primitives (`det`, `rank`, `trace`, …) or arrays
256
+ (`row`, `col`, `eigenvalues`) must NOT be in the set.
257
+ 3. `**str.format(fmt, ...)`** uses `_infer_type` (codegen/types.py) to
258
+ decide whether to wrap each arg in `std::to_string`. Source-text
259
+ prefix heuristics (`"`, `std::string`, `pine_str`) are NOT
260
+ sufficient; bare identifiers and bound results lose their
261
+ string-ness. Booleans go through a TV-style ternary
262
+ (`(v ? "true" : "false")`) so backtest logs match TradingView.
263
+ 4. **Per-call-site TA cloning.** Multiple `ta.sma(close, ...)` call
264
+ sites need separate `ta::SMA` instances (one per call site),
265
+ addressed via `_cs0`, `_cs1`, … suffixes. The analyzer assigns
266
+ call-site indices in `ctx.func_call_cs_map`; the codegen's
267
+ `_active_call_site_idx` machinery threads them through user-defined
268
+ functions. When adding TA dispatch, make sure the new path respects
269
+ `cs_info` / `_func_cs_var_remap`.
270
+ 5. `**Series<T>` ring buffer.** Bar-related fields (`close`, `high`,
271
+ `low`, `open`, `volume`, derived `hl2/hlc3/ohlc4/hlcc4`) auto-promote
272
+ to `_s_<name>` series whenever the script reads them with `[k]`. The
273
+ analyzer registers them in `ctx.series_bar_fields`; the codegen
274
+ declares `Series<double> _s_close;` etc. and pushes the current bar's
275
+ value at the top of `on_bar`. `pivot_point_levels` always reads the
276
+ PREVIOUS bar's HLC (`_s_high[1]`, `_s_low[1]`, `_s_close[1]`) per
277
+ Pine v6 semantics with `developing=false`.
278
+ 6. **`request.security` is strict.** Only `symbol`, `timeframe`,
279
+ `expression`, `gaps`, and `lookahead` are allowed. Symbol must resolve
280
+ to the current chart symbol (`syminfo.tickerid` or `syminfo.ticker`).
281
+ `gaps` and `lookahead` must be the literal `barmerge.gaps_*` /
282
+ `barmerge.lookahead_*` member access (codegen does not parse other
283
+ shapes). `barmerge.lookahead_on` is hard-rejected for backtest-honesty
284
+ reasons. The `timeframe` argument, when a string literal, is validated
285
+ against the Pine v6 TF format at parse time (PR #3).
286
+ 7. `**SUPPORTED_LOG`** gates `log.{info,warning,error}`. Without it,
287
+ typos like `log.foo("x")` previously emitted a dead empty-string
288
+ statement. Don't remove the gate.
289
+ 8. `**CLOSED_TRADE_ACCESSOR_METHODS` vs `OPEN_TRADE_ACCESSOR_METHODS`**
290
+ are intentionally asymmetric. `opentrades` has no `exit_*` fields
291
+ in Pine v6; both lack `direction`. `TRADE_ACCESSOR_METHODS` is kept
292
+ as the union for back-compat but new code should prefer the side-
293
+ specific constant.
294
+
295
+ ## How to add a new Pine v6 function
296
+
297
+ Worked example: adding hypothetical `ta.foo(source, length)`.
298
+
299
+ 1. **Signature** — `signatures.py`:
300
+ ```python
301
+ _ta("foo", _sig([("source", F), ("length", I)]))
302
+ ```
303
+ 2. **Analyzer dispatch** — `analyzer/tables.py`:
304
+ ```python
305
+ TA_CLASS_MAP["foo"] = "ta::Foo"
306
+ TA_PERIOD_ARG["foo"] = 1 # length-arg index
307
+ ```
308
+ 3. **Codegen dispatch** — `codegen/tables.py`:
309
+ ```python
310
+ TA_COMPUTE_ARGS["foo"] = [0] # which positional args go to .compute()
311
+ ```
312
+ 4. **Support whitelist** — `support_checker.py`:
313
+ `SUPPORTED_TA` is derived from `TA_CLASS_MAP` automatically; nothing
314
+ to do.
315
+ 5. **Surface lock** — `tests/test_official_surface.py`:
316
+ add `"foo"` to `OFFICIAL_TA` (if you skipped this step, the test
317
+ still passes for `ta.`* because it's in `test_ta_official_surface.py`
318
+ — keep both files in sync).
319
+ 6. **Smoke** — `tests/test_ta_official_surface.py::TA_SMOKE_CASES`:
320
+ add `"foo": ("x = ta.foo(close, 5)", "ta::Foo")`.
321
+ 7. **Engine** — `ta::Foo` class must exist in
322
+ `pineforge-engine/include/pineforge/ta.hpp` with both `compute()`
323
+ and `recompute()` methods. If it doesn't, the addition belongs in
324
+ the engine repo first.
325
+ 8. Run `pytest` (passes without engine env) and
326
+ `PINEFORGE_ENGINE_INCLUDE=… pytest` (passes with the engine
327
+ compile sweep) before opening a PR.
328
+
329
+ ## How to run tests
330
+
331
+ See "REQUIRED before claiming any change is done" at the top of this
332
+ file for the mandatory verification path. Recap:
333
+
334
+ ```bash
335
+ # Quick dev loop — pure transpiler, zero native deps. < 1 s.
336
+ # Use during development for fast iteration. NOT sufficient to claim
337
+ # a change is done — the 237 compile tests skip in this mode.
338
+ pytest
339
+
340
+ # REQUIRED before claiming any change is done. ~55 s.
341
+ export PINEFORGE_ENGINE_INCLUDE=/path/to/pineforge-engine/include
342
+ pytest
343
+
344
+ # Subset shortcut — corpus sweep alone (~47 s) when iterating on a
345
+ # change that you suspect specifically affects corpus coverage.
346
+ pytest tests/test_compile_corpus.py
347
+ ```
348
+
349
+ Expected counts at HEAD:
350
+
351
+
352
+ | Mode | passed | skipped | failed |
353
+ | ------------------------------------------------------- | ------ | ------- | ------ |
354
+ | With sibling engine auto-detected (or env var set) | 944 | 1 | **0** |
355
+ | Without engine (no sibling, no `PINEFORGE_ENGINE_INCLUDE`) | varies | 237+ | **0** |
356
+
357
+ The 1 skip is `test_parser.py:335` (empty parameter set, pre-existing,
358
+ unrelated). When no engine include is resolvable, the 237 compile-only
359
+ tests (31 smoke + 206 corpus) skip cleanly. Auto-detection: `tests/_compile.py`
360
+ walks up to 8 directory levels looking for a `pineforge-engine/include` sibling
361
+ — no env var needed when the engine repo is checked out at `../pineforge-engine`.
362
+
363
+ ## Conventions
364
+
365
+ - **Type system.** `PineType` (in `symbols.py`) is intentionally small
366
+ and aligns with TradingView's primitive types. For collection /
367
+ composite types use `TypeSpec`. Codegen's `_infer_type` returns a C++
368
+ type STRING (e.g. `"std::string"`, `"PineMatrix"`) — that's what most
369
+ emission paths consume.
370
+ - **Errors.** Use `errors.CompileError` for fatal issues raised from
371
+ the transpiler. Carry `SourceLocation` so users can map back to the
372
+ Pine line/col. Diagnostics inside the support checker use
373
+ `Level.WARNING` for divergences-but-not-broken, `Level.ERROR`
374
+ otherwise.
375
+ - **Comments in emitted C++.** When emitting a fallback / unsupported
376
+ stub, include a `/* unsupported: ... */` marker in the source so a
377
+ later compile error has context. Avoid emitting bare empty literals.
378
+ - **Helper underscores.** Codegen-internal helpers in `codegen/tables.py`
379
+ are underscore-prefixed (`_matrix_add_row`, `_merge_kwargs`); they
380
+ are not part of the package's external surface.
381
+ - **Reserved names.** `codegen/helpers.py::CPP_RESERVED` carries the C++
382
+ keyword set; `_safe_name` rewrites Pine identifiers that collide.
383
+
384
+ ## Safety rules for AI agents working in this repo
385
+
386
+ - **Never silently widen `SUPPORTED_*`.** Every addition needs a
387
+ matching entry in the per-namespace official set in
388
+ `tests/test_official_surface.py`, or it breaks the surface lock-in.
389
+ - **Never delete a `test_regression_*` case** without first
390
+ understanding which once-broken codepath it pins. The xfail->pass
391
+ history is intentional.
392
+ - **Always finish with `PINEFORGE_ENGINE_INCLUDE=... pytest`.** See the
393
+ "REQUIRED before claiming any change is done" block at the top.
394
+ A diff that passes the pure-transpiler tests but fails on 1 / 206
395
+ corpus strategies (or 1 / 31 compile smokes) is still a regression.
396
+ Don't report a change as done until the 944-pass run is green.
397
+ - **Don't update the version in `pyproject.toml`** without confirming
398
+ the engine ABI tag listed in the README's version table actually
399
+ exists upstream and exposes the symbols we emit.
400
+ - **Don't introduce runtime dependencies.** Pure-Python is the install
401
+ contract. Test extras (pytest) are the only allowed `[project.optional-dependencies]`.
402
+