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.
- pineforge_codegen-0.6.5/.github/dependabot.yml +8 -0
- pineforge_codegen-0.6.5/.github/workflows/release.yml +123 -0
- pineforge_codegen-0.6.5/.gitignore +122 -0
- pineforge_codegen-0.6.5/CLAUDE.md +402 -0
- pineforge_codegen-0.6.5/LICENSE +197 -0
- pineforge_codegen-0.6.5/PKG-INFO +462 -0
- pineforge_codegen-0.6.5/README.md +243 -0
- pineforge_codegen-0.6.5/VERSION +1 -0
- pineforge_codegen-0.6.5/docs/codegen-coverage-gaps.md +78 -0
- pineforge_codegen-0.6.5/pineforge_codegen/__init__.py +53 -0
- pineforge_codegen-0.6.5/pineforge_codegen/analyzer/__init__.py +60 -0
- pineforge_codegen-0.6.5/pineforge_codegen/analyzer/base.py +1563 -0
- pineforge_codegen-0.6.5/pineforge_codegen/analyzer/call_handlers.py +895 -0
- pineforge_codegen-0.6.5/pineforge_codegen/analyzer/contracts.py +163 -0
- pineforge_codegen-0.6.5/pineforge_codegen/analyzer/diagnostics.py +118 -0
- pineforge_codegen-0.6.5/pineforge_codegen/analyzer/tables.py +204 -0
- pineforge_codegen-0.6.5/pineforge_codegen/analyzer/types.py +250 -0
- pineforge_codegen-0.6.5/pineforge_codegen/ast_nodes.py +293 -0
- pineforge_codegen-0.6.5/pineforge_codegen/codegen/__init__.py +78 -0
- pineforge_codegen-0.6.5/pineforge_codegen/codegen/base.py +1381 -0
- pineforge_codegen-0.6.5/pineforge_codegen/codegen/emit_top.py +875 -0
- pineforge_codegen-0.6.5/pineforge_codegen/codegen/helpers.py +163 -0
- pineforge_codegen-0.6.5/pineforge_codegen/codegen/helpers_syminfo.py +134 -0
- pineforge_codegen-0.6.5/pineforge_codegen/codegen/input.py +189 -0
- pineforge_codegen-0.6.5/pineforge_codegen/codegen/security.py +1564 -0
- pineforge_codegen-0.6.5/pineforge_codegen/codegen/ta.py +298 -0
- pineforge_codegen-0.6.5/pineforge_codegen/codegen/tables.py +613 -0
- pineforge_codegen-0.6.5/pineforge_codegen/codegen/types.py +573 -0
- pineforge_codegen-0.6.5/pineforge_codegen/codegen/visit_call.py +1305 -0
- pineforge_codegen-0.6.5/pineforge_codegen/codegen/visit_expr.py +701 -0
- pineforge_codegen-0.6.5/pineforge_codegen/codegen/visit_stmt.py +729 -0
- pineforge_codegen-0.6.5/pineforge_codegen/errors.py +98 -0
- pineforge_codegen-0.6.5/pineforge_codegen/lexer.py +531 -0
- pineforge_codegen-0.6.5/pineforge_codegen/parser.py +1198 -0
- pineforge_codegen-0.6.5/pineforge_codegen/pragmas.py +117 -0
- pineforge_codegen-0.6.5/pineforge_codegen/signatures.py +808 -0
- pineforge_codegen-0.6.5/pineforge_codegen/support_checker.py +1111 -0
- pineforge_codegen-0.6.5/pineforge_codegen/symbols.py +118 -0
- pineforge_codegen-0.6.5/pineforge_codegen/tokens.py +406 -0
- pineforge_codegen-0.6.5/pineforge_codegen/tv_input_choices.py +86 -0
- pineforge_codegen-0.6.5/pyproject.toml +55 -0
- pineforge_codegen-0.6.5/tests/__init__.py +0 -0
- pineforge_codegen-0.6.5/tests/_compile.py +176 -0
- pineforge_codegen-0.6.5/tests/golden/matrix_eigen_pca.cpp +299 -0
- pineforge_codegen-0.6.5/tests/test_analyzer.py +221 -0
- pineforge_codegen-0.6.5/tests/test_analyzer_matrix_inference.py +77 -0
- pineforge_codegen-0.6.5/tests/test_analyzer_ta_return_types.py +108 -0
- pineforge_codegen-0.6.5/tests/test_codegen_fallthrough_guards.py +137 -0
- pineforge_codegen-0.6.5/tests/test_codegen_golden.py +91 -0
- pineforge_codegen-0.6.5/tests/test_codegen_input_getters.py +207 -0
- pineforge_codegen-0.6.5/tests/test_codegen_matrix_typed.py +412 -0
- pineforge_codegen-0.6.5/tests/test_codegen_new.py +1215 -0
- pineforge_codegen-0.6.5/tests/test_compile_corpus.py +145 -0
- pineforge_codegen-0.6.5/tests/test_compile_smoke.py +613 -0
- pineforge_codegen-0.6.5/tests/test_errors.py +33 -0
- pineforge_codegen-0.6.5/tests/test_input_time_int64.py +39 -0
- pineforge_codegen-0.6.5/tests/test_int64_time_storage.py +56 -0
- pineforge_codegen-0.6.5/tests/test_lexer.py +57 -0
- pineforge_codegen-0.6.5/tests/test_official_surface.py +426 -0
- pineforge_codegen-0.6.5/tests/test_parser.py +342 -0
- pineforge_codegen-0.6.5/tests/test_security_tf_literal.py +99 -0
- pineforge_codegen-0.6.5/tests/test_signatures.py +1000 -0
- pineforge_codegen-0.6.5/tests/test_support_checker.py +645 -0
- pineforge_codegen-0.6.5/tests/test_support_checker_chart_visible_bar_time.py +18 -0
- pineforge_codegen-0.6.5/tests/test_support_checker_color_cast.py +17 -0
- pineforge_codegen-0.6.5/tests/test_support_checker_dividends_earnings.py +28 -0
- pineforge_codegen-0.6.5/tests/test_support_checker_footprint.py +26 -0
- pineforge_codegen-0.6.5/tests/test_support_checker_input_color.py +75 -0
- pineforge_codegen-0.6.5/tests/test_support_checker_input_source.py +73 -0
- pineforge_codegen-0.6.5/tests/test_support_checker_matrix.py +55 -0
- pineforge_codegen-0.6.5/tests/test_support_checker_security_adjustment.py +76 -0
- pineforge_codegen-0.6.5/tests/test_support_checker_timeframe_from_seconds.py +21 -0
- pineforge_codegen-0.6.5/tests/test_support_checker_varip.py +53 -0
- pineforge_codegen-0.6.5/tests/test_support_checker_volume_row.py +21 -0
- pineforge_codegen-0.6.5/tests/test_symbols.py +42 -0
- pineforge_codegen-0.6.5/tests/test_ta_official_surface.py +163 -0
- pineforge_codegen-0.6.5/tests/test_transpile_division.py +115 -0
- pineforge_codegen-0.6.5/tests/test_transpile_enum_order.py +29 -0
- pineforge_codegen-0.6.5/tests/test_transpile_pf_trace.py +219 -0
- pineforge_codegen-0.6.5/tests/test_transpile_tr_handle_na.py +124 -0
- pineforge_codegen-0.6.5/tests/test_transpiler_matrix_kwargs.py +17 -0
- pineforge_codegen-0.6.5/tests/test_typespec_matrix.py +17 -0
- pineforge_codegen-0.6.5/tests/test_udt_drawing_field_cleanup.py +103 -0
- pineforge_codegen-0.6.5/tests/test_unsupported_reporting.py +178 -0
- pineforge_codegen-0.6.5/tests/test_vwap_tuple_unpack.py +146 -0
|
@@ -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
|
+
|