Skip to content

History of user-defined objects and drawing references - #156

Merged
luisleo526 merged 9 commits into
mainfrom
cg/udt-history
Oct 1, 2026
Merged

luisleo526 merged 9 commits into
mainfrom
cg/udt-history

Conversation

@luisleo526

Copy link
Copy Markdown
Contributor

History references on user-defined objects and drawing references

codegen 1.0.0 emitted C++ that did not compile for the history of an object: a variable of a user-defined type or of a drawing type whose history was read (obj[1]) was declared Series<double> and the object handles were pushed into it. The four probes of the blog fact-check (t13_ne_hist, t14a_udt_hist_typed, t14b_udt_hist_untyped, t14c_udt_hist_var) failed with "no viable conversion from 'Cell' to 'double'" and "no member named 'push' in 'Acc'".

What TradingView does

Checked against TradingView itself (synthetic probes and their TradingView trade exports in tests/fixtures/udt_history_tv, see its README) and the Pine v6 User Manual:

  • "Type system", Value vs. reference types: user-defined types and the drawing types are reference types; "Variables of reference types hold these object references; they do not store objects directly." "Objects", Copying objects: objects are assigned by reference.
  • obj[k] is the reference the variable held k bars back, na before the first bar, and (obj[k]).field reads that object as it is now: (acc[1]).total of a var object equals acc.total, and a change made through c[1] shows through (c[2]) on the next bar (udth_ref). Boxes, lines and labels alike, a box deleted through b[1] included (udth_box, udth_box2).
  • In a function, a method and an if block the history counts the executions of the scope (udth_fn, udth_method, udth_fn2). At a call site that skips bars, a parameter's and a receiver's history is one slot per chart bar holding the last call's value: x[2] at a call on every third bar reads the call of two bars back, not two calls back (udth_clock, udth_clock_method).
  • Drawings through parameters: a box receiver's or parameter's history read with a built-in method, (this[1]).get_top(), is the previous call's box, and a drawing passed to a drawing parameter is the drawing itself, a history read (f(b[1])), a var box's history, a new box and a line beside its history included (udth_drawparam).
  • The history of an expression whose value is an object (o.inner[1], f()[1], (c ? a : b)[1]) is the reference it produced at its previous evaluation (udth_expr), and below a lazy edge it is kept on every bar, the bars a ternary's selector skips the arm included (udth_lazy: the arm is skipped on every even bar, and each odd bar reads the even bar before it).
  • == / != compare line and label references by identity, na included (udth_line_eq); TradingView refuses them for every other reference type (CE10123) and a comparison with na (CE10187).
  • Spellings TradingView refuses: a field or method straight after the history operator, c[1].v (CE10011) and b[1].get_top() (CE10010) -- "Migration guide to v6", History of UDT fields: "use the syntax (myObject[10]).field - ensure the object's historical reference is wrapped in parentheses, otherwise it is invalid" -- and the history of a field holding a value, c.v[1] and (c.v)[1] (CE10290). A field read of a na object stops the run (RE10041). So the four blog probes are TradingView compile errors as written; their parenthesized forms are the valid spelling.

The change

  • A history-read variable holding an object or a drawing is a Series of its handles (Series<Cell>, Series<Box>; na is the handle's T{}): globals, var objects, block and function locals (with their per-call-site copies), history parameters and method receivers. (c[1]).v reads the arena record the handle names; a drawing parameter's or receiver's history takes its built-in methods ((x[1]).get_top()).
  • A temporary drawing passed to a drawing parameter, which a callable takes as T& -- a history read, a history-read variable's current slot, a new drawing or chart point, a user call's result -- is bound through a staged call, as a fresh array already is. Variables, fields and collection elements keep their C++.
  • The history of an object-valued expression keeps a Series of handles; below a lazy edge a read that is safe to evaluate (a pure or object-constructor call over pure arguments, a ternary over names, a named object's field) is pushed on every execution, as TradingView keeps it.
  • A field chain now visits its receiver in the analyzer: (o[1]).inner.v registered no history and silently read the current object.
  • == / != of two lines or two labels compare handle ids.
  • Refused with a located diagnostic, as TradingView refuses them: x[k].field / x[k].method() (CE10011 / CE10010, hint (x[k]).field), reference comparisons other than line/label (CE10123) and with na (CE10187), the history of a value field (CE10290). Refused here (they never compiled): the history of a reference inside a request.security expression (TradingView reads the requested timeframe's objects, which PineForge does not keep), and the history of a chart.point variable whose fields the script changes (PineForge holds a point as a value).
  • CLAUDE.md / AGENTS.md gain an "Object and drawing history" support-contract row.

No refusal applies to a script that compiled before: each one either is a spelling TradingView rejects or produced C++ that did not compile.

Tests

  • tests/test_udt_history.py: every shape compiles (42 shapes: globals, var, locals, parameters, receivers, drawing parameters' and receivers' methods, temporary drawings passed to drawing parameters, call results, selections, fields, boxes, lines, labels, chart points, two call sites), the emitted Series types, and every refusal with its TradingView code, including the probes TradingView's compiler refused.
  • tests/test_e2e_udt_history.py: eleven TradingView tapes replay exit for exit (336 of 336 each); bar-by-bar traces of obj[1], (obj[1]).field, a change through history, l != l[1] and a drawing parameter's and receiver's history read with a built-in method; a na object's field stopping the run; the pre-change build failing to compile every tape probe; and the receiver-clock gap below, pinned.
  • tests/test_e2e_drawing_lifetime.py: w9dg-pin-holders2 (a box carried by s := s[1]), evidence-only until now because it did not compile, replays TradingView's tape.
  • tests/test_codegen_matrix_typed.py: the matrix history test spells its read (m[1]).get(0, 0), which lowers to the same C++ as before; m[1].get(0, 0) is a TradingView compile error.

Run against main 01b4ee3, the new tests fail 76 and error 12 of 103; the 15 that pass there are the tape-content checks, the pre-change-build checks and three shapes that already compiled (a var box's history, a requested field value, a request without an expression). After the change all pass.

Gates

The full suite on a shared x86-64 build host (Ubuntu, GCC 13), against engine 6b45f510 (v1.0.0 and two docs commits), at this branch's head: pytest 4783 passed, 0 failed, 33 skipped (28 need an older commit the shipped checkout's history lacks, 5 empty parameter sets), junit tests=4816 failures=0; tests/test_compile_corpus.py 314 passed, 0 skipped; the Pyodide gate's fixtures PARITY OK over 277 (ok 264/264, err 13 rejected as expected).

Population: the transpiler's C++ for every one of the 1,384 unique population sources is byte-identical on main and on this branch's head (also on its earlier head bf9134f), transpiled in fresh processes; within one long census process two sources differ only by the known ordering flake of two .clear() lines. None of them reads an object's or drawing's history. An experiment on bf9134f measured the 208 population probes (58 sources) that declare a user-defined type, across 17 lanes: all 208 excellent, each with the tier and match % of the active baseline; the later commits leave every population source's C++ unchanged.

Not covered:

  • A typed method's receiver at a call site that skips bars counts calls, where TradingView keeps one slot per chart bar: from the third call on, this[2] reads three bars further back (udth_clock_method, 111 of 336 closes; pinned by a test). Typed methods kept their own bridge when fix: hold last values for history-reading UDF series parameters at skipped callsites #109 moved plain functions' parameters to TradingView's clock; this lane's object parameters already have it (udth_clock replays).
  • A UDT constructor given a non-constant int for a float field (Cell.new(v = bar_index)) narrows in a braced initializer and does not compile.
  • The history of a collection ((a[1]).size(), (m[1]).get(0, 0)) and a method on an operator expression ((close * 2).m()) emit None.
  • h.xs[1] on an array field keeps its current-collection lowering although TradingView refuses it (CE10290), as it refuses a drawing field's h.ln[1], which PineForge already refused.
  • Dead Series members for history-read parameters (a receiver's pf_safe_this and its call-site copies) remain, as for every parameter type before; their copies are now typed like their base.

🤖 Generated with Claude Code

luisleo526 and others added 9 commits October 1, 2026 09:00
A variable of a user-defined type or a drawing type holds a reference,
and its history holds the references it held (TradingView tapes in
tests/fixtures/udt_history_tv). Such a history was declared
Series<double> and the handles pushed into it, which did not compile:
it is now a Series of the handles (Series<Cell>, Series<Box>; na is
T{}) for globals, var objects, block and function locals, history
parameters and method receivers. The history of an expression whose
value is an object (a field holding one, a call's result, a ternary's
selection) is the reference it produced at its previous evaluation,
kept on every execution below a lazy edge where that is safe. A field
chain now visits its receiver, so (o[1]).inner.v reads the history of
o instead of the current object.

== and != compare line and label references by identity, as
TradingView does; every other object or drawing comparison and a
comparison with na are refused as TradingView refuses them (CE10123,
CE10187). Refused as TradingView refuses them: a field or a method
straight after the history operator (c[1].v, CE10011; b[1].get_top(),
CE10010) and the history of a value field (c.v[1], CE10290). Refused
here: reference history inside a request.security expression, and the
history of a chart.point whose fields the script changes.

w9dg-pin-holders2 (a box carried by s := s[1]) now replays.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
…s field stops the run

The probes TradingView's compiler refused are refused with its codes,
udth_na_field (now an int field, re-checked on TradingView: RE10041)
stops the engine's run as TradingView's stops, and the matrix history
test spells its read as TradingView takes it, (m[1]).get(0, 0), which
lowers to the C++ m[1].get(0, 0) did.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Review follow-ups. A function's history-read var object at two call
sites declared its per-call-site copies Series<double>; they hold the
handles now, as the base member does. The prepass typed the history
Series of an expression over a typed parameter (f(Outer p) =>
p.inner[1], a selection of two parameters) without the parameter
bound, which left it unregistered (an AssertionError at the hoist) or
a Series<double>: the reference check binds the owner's parameters,
emission reads the type the member was declared with, and only a
registered read is hoisted. The history of such an expression is the
reference type in _infer_type too, so a local holding it declares the
handle. A request.security with no expression (CE10165 on TradingView)
lowers to na again instead of crashing the reference-history check.

TradingView's udth_fn2 tape pins the three shapes.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
A drawing parameter or method receiver whose history is read is a
const Series<Box>& in the emitted callable, and the lexical drawing
types admitted a parameter only when its C++ type was Box or Box&, so
(x[1]).get_top() and (this[1]).get_top() found no drawing receiver and
emitted None(). The series parameter's element type admits it too.
TradingView reads the box the parameter or receiver held at the
previous call (udth_drawparam, next commit's tape).

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
A callable takes a line, box, label, linefill or chart.point parameter
as T&, and a history read (f(b[1])), a history-read variable's current
slot (f(b), emitted b[0]), a new drawing (f(box.new(...))) or a user
call's result is a C++ prvalue no T& takes: "non-const lvalue
reference cannot bind to a temporary", for plain functions and for a
typed method's receiver ((b[1]).topM(), b.topM()) alike. Such a call
is staged as a fresh array's is (_ordered_user_call_expr): each
argument binds to a named forwarding reference, in source order. The
handle is an arena id and Pine forbids reassigning a parameter, so the
callee reads the drawing the argument names. A variable, a field and a
collection element are lvalues and keep their C++.

TradingView's udth_drawparam tape (BINANCE:ETHUSDT.P 15) pins these
shapes and the previous fix's: 336 of 336 exits replay.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
udth_expr's reads sit in arms its na() guards skip on the first bar
only, so its README overstated what the tape shows about skipped arms.
TradingView's udth_lazy tape skips the arm on every even bar: each odd
bar's (o.inner[1]).v, (mk(bar_index * 7)[1]).v and selection read the
even bar before it, where the bars that took the arm would read two
bars back. The codegen's every-execution push below a lazy edge already
reads them so (336 of 336 exits, on bf9134f too); the README now says
what each tape shows.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
The review read a parameter's history at a call site that skips bars
as counting calls in TradingView. TradingView's udth_clock tape says
otherwise: at a call on every third bar an object parameter's x[2] and
a float parameter's y[2] read the argument of the call at or before two
bars back, one slot per chart bar holding the last call's value (bar
4's call on bar 7, not bar 1's) -- the clock #109 gave plain functions'
parameters, which the object parameters of this lane share: the tape
replays 336 of 336.

udth_clock_method shows a method receiver on the same clock, which the
codegen does not keep: typed methods kept their own bridge, which
pushes a receiver built at the call once per call, so from the third
call on it reads three bars further back (111 of 336 closes). The gap
is older than this lane and is pinned, not fixed here.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
A method whose receiver's history is read is emitted once per call
site, and the copy of the receiver's Series member was declared from
its escaped spelling (pf_safe_this), which names no symbol: a
Series<double> beside its base member's Series<Cell>. Both members are
unused (the method takes the receiver as a parameter); the copy now has
its base's type. Every other copy keeps its declaration.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Drawing parameters' and receivers' built-in methods, temporary drawings
passed to drawing parameters, the parameter clock at a call site that
skips bars and the receiver gap pinned beside it, skipped lazy arms,
and the eleven replayed tapes.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
@luisleo526
luisleo526 merged commit 41cc4bb into main Oct 1, 2026
9 checks passed
@luisleo526
luisleo526 deleted the cg/udt-history branch October 1, 2026 15:20
luisleo526 added a commit that referenced this pull request Oct 2, 2026
* docs: changelog and version scoping for 1.0.1

1.0.1 pairs with engine v1.0.1, which changes only documentation since
v1.0.0. The changelog section covers #156 and #157, the changes since
1.0.0 that a user can observe:

- C++ that did not compile now compiles: the history of user-defined
  objects and drawings (#156) and of arrays and matrices (#157), and a
  non-constant int given to a float field (#157);
- one value that came out silently wrong: an int na assigned to a float
  field was stored as -2147483648 and is na now (#157);
- new compile-time refusals with TradingView's codes (CE10010, CE10011,
  CE10101, CE10122, CE10123, CE10173, CE10187, CE10290, CE10009), and the
  forms refused by name;
- the "not covered" limits of both pull requests, the migration step
  (regenerate C++ and relink) and the pairing (1.0.1 supports only engine
  v1.0.1). The Python and JSON contract and the report keys do not change.

The README, CONTRIBUTING, AGENTS.md / CLAUDE.md, docs/PUBLIC_CONTRACT.md
and docs/pine-cap-activation.md name 1.0.1 where they named 1.0.0 as the
current release or pair, as the README's release-lane note asks (the
release section, the baseline paragraph, the engine src/source link, the
@pf-trace note, the clone command and its output, the timing note, the
explicit-attachment section, a 1.0.1 row in the pairing table). Lines
that say what changed in 1.0.0 stay, and no line calls a version the
latest.

Checked on this tree's code (67892cd) against v1.0.0, CPython 3.14.6 on
an Apple M4 Max: the engine corpus's 325 sources and the 277 gate fixtures
transpile to byte-identical C++ (13 refusals, identical messages); the
largest transpilation takes 0.048 s (0.049 s on v1.0.0); the quick-start
SMA cross transpiles to the same C++. The tutorial run with that C++
against engine 6b45f510 (Linux arm64, GCC 13) prints the README's 9 trades,
+738.20, and 13 trades with the old sizing declared.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* docs: the 1.0.1 code was graded before it merged, with no tier change

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QFVRQ4gcJCLbjfYqGtmy4k

---------

Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant