84 Errors Became 5: The Version-to-Version Table for a Pine-to-Python Translator
TL;DR: A translator that went from 84 failed scripts to 5 did not get 16 times better in one fix. It got better in three recorded stages — 84 → 13 → 7 → 5 — across nine compile checkpoints and eight run checkpoints, with a per-script diff after every batch proving nothing that used to pass started failing. The headline number, 99.7% in our testing, is the least interesting part. The table is the evidence — and the table includes the one number that got worse, and the one sweep that lied.
This is the ninth post in a series where I convert Pine strategies to Python line by line. The last post was about a single fix — the when= desugar — measured trade for trade. This one steps back to the sweep that produced all those fixes, because a single percentage hides the only question that matters: did it get better, version by version, without anything silently getting worse?
The short version
- Compile: 1,827 of 1,911 scripts compiled on 2026-10-21 (95.6%), with 84 analyze failures. By the v45 baseline on 2026-10-24: 1,904 of 1,909 (99.7%), with 5 failures.
- Run: clean runs went from 1,859 to 1,887 of 1,904 (99.1%), and run errors fell from 45 files to 17 — 9 unique scripts in 7 families.
- The three stages: 84 → 13 (the big construct families) → 7 (three two-file fixes) → 5 (one overload fix). The last three fixes were two files each.
- The number that got worse: diagnostics rose from 41 to 55 while failures fell. That is not a regression — it is the price of compiling more scripts, and the post explains why.
-
Zero regressions is a measured claim, not a boast: after every batch, a per-script diff (
ok → errormust be 0) ran before anything was committed. The totals never moved without the diff agreeing. -
The sweep that lied: one full sweep reported
ran_ok1,428 — invalid data, deleted, baseline reset. The table below is what survived the rule that caught it.
The compile table: nine checkpoints, one direction
Each row is a recorded checkpoint from the project log. The "what changed" column is the batch that moved the number; the delta is the sweep total, not a promise about the next batch.
| Checkpoint | Compiled | Failures | What changed |
|---|---|---|---|
| 2026-10-21 evening baseline | 1,827 / 1,911 | 84 | Starting point: 95.6% |
security batch |
1,850 / 1,911 | 61 |
request.security() tuple literals (17 files) and scalar expressions (8) |
for-in batch |
1,858 / 1,911 | 53 | Iterator typing for for...in (8) |
| namespace batch | 1,874 / 1,911 | 37 | Namespace field access (16) + UDT method calls |
| continuation batch | 1,890 / 1,911 | 21 | Line-continuation and indentation parsing (8) |
chart.point batch |
1,894 / 1,911 | 17 |
chart.point field reassignment |
table.cell batch |
1,896 / 1,911 | 15 | table.cell_set_text_formatting |
| batch 2 (§16.7) | 1,902 / 1,909 | 7 | Tuple-switch tail returns, drawing kwargs, chained receivers (2 files each) |
| batch 2b (§16.8) | 1,904 / 1,909 | 5 |
int[]/float[] overload dispatch on 012_volume-profile-maps (2 files) |
Three things to notice before the numbers do any persuading:
First, the denominator moved. The corpus went from 1,911 files to 1,909 because two illegal inputs were quarantined out of the corpus entirely (M4). Those two were never "fixed" — they were removed from what counts. So the failure count fell by 79, and that 79 is exactly 77 scripts newly compiling plus 2 files removed from the corpus. If you only read the percentage, 95.6% → 99.7%, you miss the two files that left through the side door. They are in the log; they are not hidden.
Second, the fix ledger and the checkpoints disagree by two files — and the post says so. The per-construct fix log lists twelve groups totalling 81 files — 17 + 16 + 12 + 8 + 8 + 8 + 2 + 2 + 2 + 2 + 2 across eleven fix groups, plus the 2 quarantined inputs — against a net failure drop of 79. The two-file gap is the difference between counting fixes and counting files: the ledger counts per construct, so a file touched by two fixes counts twice, and the corpus holds duplicate copies of the same script under different folders. The ledger is a fix log, not a partition of the total. The sweep totals are the numbers that reconcile; the ledger is the map of what was touched. A translator that hides a two-file gap in its own accounting is exactly the kind of tool you should not trust — so here is the gap, in the open.
Third, the fixes got smaller. The early batches moved 23 and 16 files at a time. The last three fixes moved two files each. That is what the end of a fix journey looks like: not a breakthrough, but a narrowing. When the remaining failures are three unique scripts — two waiting on external evidence before anyone touches the runtime, one waiting on a library import — the work has moved out of the translator and into the scripts, the data, and the runtime contract underneath.
The run table: compiling is not running
A script can compile cleanly and still fail bar by bar. The sweep ran every compiled script, so there is a second table — and it is the one with the stricter claim, because a run either finishes or it does not.
| Version | Clean runs | Run errors | Unique scripts | Families |
|---|---|---|---|---|
| v28 | 1,859 | 45 | 25 | 17 |
| v33 | 1,861 | 43 | 24 | 17 |
| v34 | 1,873 | 31 | 18 | 12 |
| v35 | 1,875 | 29 | 17 | 11 |
| v36 | 1,880 | 24 | 14 | 10 |
| v37 | 1,882 | 22 | 13 | 9 |
| v39 | 1,884 | 20 | 12 | 9 |
| v44 | 1,887 | 17 | 9 | 7 |
v45 is identical to v44 on every metric — its only change was a matrix field annotation, and the generated code did not change by a single byte (changed python 0). A version that changes nothing measurable is still a row in the table, because a table that only shows the versions that improved is a brochure, not a log. The same goes for v38, a measurement-only batch whose generated code was byte-identical to v37: the numbers did not move, and the log says so instead of skipping the version.
The family count fell from 17 to 7, and the shape of that fall is the finding: families 2, 4, and 5 disappeared entirely (a feed session gap, three strategies fixed by a runtime patch, a missing 12M feed), while the seven that remain are, as post #3 tabulated, scripts that are broken on TradingView too, scripts that are not supposed to run on this feed, and one structural limit around history inside helper functions. The fixable bucket is empty — that was #3's conclusion, and this table is the version-by-version proof of how it got emptied.
The number that got worse: diagnostics 41 → 55
Here is the row a brochure would delete. While failures fell from 84 to 5, the diagnostic count — warnings attached to scripts that compile but are not fully, faithfully translated — rose from 41 to 55.
That is not a regression. It is arithmetic with a cause: 77 scripts that previously failed analysis now compile, and 14 of the diagnostics they carry came in with them. The single largest group, 47 of the 55, is history reads inside module-level helpers (zigzag, renderStructures, backfill — a helper that reads high[1] from inside a function, where the call site can only pass plain values). Those scripts used to fail outright; now they compile with a warning that says exactly where the translation stops being faithful. A warning is better than a failure, and a failure is better than silence — the ranking matters, and the diagnostic count going up while the failure count goes down is what that ranking looks like in numbers.
The check that it is not a regression is the companion number: scripts translating with zero diagnostics rose from 1,806 to 1,879 (98.4% in our testing). Both numbers moved in the honest direction at once — more scripts fully clean, more warnings on the ones that are not. If diagnostics had risen while the zero-diagnostic count fell, that would be a real regression, and this post would say so.
There is a second number that "improved" without anything getting better, and it deserves the same treatment. When the two illegal inputs were quarantined, the compile rate went from 1,896/1,911 (99.2%) to 1,896/1,909 (99.3%) — the rate rose 0.1 points and not one script got better. Denominator changes are free percentage points. Any vendor quote that cannot tell you whether its denominator moved is decoration.
Zero regressions — and what that claim actually rests on
"Zero regressions" is easy to say and easy to fake: totals can hide a script that broke while another one got fixed. The claim here rests on a tool, not on the totals. After every batch, a per-script diff compared the old and new sweep file by file — compile flips, generated-code hashes, run flips, plot counts — and any ok → error flip failed the batch with exit code 1. Totals were never accepted without the diff agreeing.
The recorded diffs:
| Batch | ok → error |
error → ok |
Generated code changed |
|---|---|---|---|
| v19 → v20 (block scope) | 0 | 0 | 11 files, all one batch's 6 scripts |
| v20 → v21 (parameter routing) | 0 | 2 (one script, both copies) | 2 files, both the same script |
| v24 → v26 (batch 2) | 0 | 0 | 6 files — exactly the three fixed scripts |
| v26 → v27 (batch 2b) | 0 | 0 | 2 files — only the fixed script's copies |
| v42 → v44 (strategy namespace) | 0 | 3 (three strategies) | 4 files, each explained |
Two details in that table are worth more than the zeros. First, v20 → v21 changed a script's error without changing its compile status — one script swapped one runtime error for a clean run, and the diff caught that the plot count moved 0 → 5 on the same script, which was itself investigated before the batch was accepted. Second, batch 2 fixed three scripts whose ok status did not change at all — and the diff still flagged their six generated files as changed, which is how the next section was found.
The sample gate ran alongside: 8 out of 8 declared samples, byte-identical output, after every batch. A sample that drifts is a regression the sweep totals would never see.
The fixes the table cannot see
Batch 2 (§16.7) is the best argument in this whole post for why totals are not enough. It fixed three scripts — tuple-switch tails, untyped drawing kwargs, chained receivers — and the compile count went up by exactly 6, three scripts times two copies. But the same batch also fixed something the totals never showed: a silent mis-translation in six files whose ok status was unchanged before and after.
The bug was in how free methods were registered: by method name only. Two methods with the same name on different owners — line arrays and label arrays both calling clear — overwrote each other, so a line array silently called the label version of the delete. It ran fine. It did nothing. No error, no diagnostic, no failed run — a script that compiled, ran, and quietly skipped its own cleanup. The fix routed methods by owner plus name, and the generated code changed in six files that the sweep had been counting as healthy all along.
That is the honest limit of this post's own table: every number in it counts failures, and a silent wrong value is not a failure. The version-to-version table proves the translator stopped crashing. It cannot prove it stopped being quietly wrong — only the per-file diffs, read one by one, catch that. The six files were caught because someone read the diff instead of the total.
The sweep that lied
One row is missing from the run table, and it is missing on purpose: v43. Its full sweep reported ran_ok 1,428 out of 1,904 — a collapse of nearly 460 scripts in a single version. It was invalid data. During the sweep, the runtime file the workers import was halfway through an edit, so chunk workers loaded a broken module and failed scripts that had nothing wrong with them. The report was deleted, the baseline was reset to v44, and the rule it produced is now written down: no edits to the runtime or the sweep runner during a full sweep — the environment fingerprint is snapshotted at startup and cannot catch a file changed mid-run.
There was an earlier, quieter version of the same lesson: the output folder out/corpus_gen once held 1,819 stale generated files while the current version's tree held 1,896. Running a sweep against the stale folder measures a three-day-old translator and reports it as today's. The fix was procedural, not technical — every batch's tree gets its own versioned folder, and the sweep must point at the same tree the report names. Both incidents are in the project log, and both are why this post trusts the table: not because the numbers are good, but because the two times the numbers lied, the lie was caught, recorded, and turned into a rule before the next batch.
What a healthy table looks like
Borrowing the calibration idea from this week's reading: a version-to-version table is healthy when every row satisfies all four of these, and any row that fails one of them is investigated before the next batch starts.
- Compiled never decreases. A version that compiles fewer scripts than the last one is a regression until proven otherwise — the diff, not the author, decides which.
-
ok → erroris always 0. New fixes may turn errors into clean runs; they may never turn a clean run into an error. Exit code 1 enforces it. - Every changed file is explainable in one sentence. Batch 2 changed six files and all six were the three fixed scripts. A changed file with no explanation is an unexamined change.
- Diagnostics may rise, but only with a reason attached. Newly compiling scripts bring their own warnings. If diagnostics rise on scripts that already compiled, something regressed.
The translator's table passes all four across the recorded window — and the two times it did not (the stale tree, the mid-edit sweep), the failures were in the measurement, not the translator, and they are recorded here rather than smoothed over.
Rules I now follow
- A percentage is a snapshot; a table is a claim. 99.7% tells you where the translator stands. 84 → 13 → 7 → 5 tells you how it got there, and whether anything broke on the way.
- Totals hide one-for-one swaps. Diff per script, every batch. A version that fixes one script and breaks another shows up as "no change" in the totals. The diff is the only thing that sees it.
- Count the denominator. A rate that rises because files left the corpus is not an improvement. Say which files left and why.
- A warning is better than a failure, and a failure is better than silence. Diagnostics rising while failures fall is progress, not decay — as long as the zero-diagnostic count rises with it.
- Record the measurements that lied. The v43 sweep and the stale tree are in the log because the next person to run a sweep needs to know how a sweep can lie, not just that this one once did.
Next in the series: the biggest family in the fix ledger — request.security(), 25 files, one construct. A tuple literal, a scalar expression, and the script that used to fail analysis and now compiles.
What does your version-to-version table look like — and when did a total hide a regression from you? I read every comment — the measurement stories are what the next posts are built from.
This conversion work was done by Pine2Py, the tool I build for exactly this job: paste a Pine script, get PyneCore-flavored Python back, diagnostics included when something can't be translated. The runtime is the open-source PyneCore project, with a patched build we ship for the two constructs stock PyneCore doesn't handle — details are on the site. It's free to try — every account gets 3 conversions, no card — and Pro is $12/month if you end up converting a whole library. No return promises, ever: a translator moves your logic, it doesn't make it profitable.
Top comments (0)