Skip to content

Integer parameters of user functions are emitted as 32-bit int, so timestamps passed through them wrap silently #285

Closed

Description

@letiep97

Summary

A user function that receives a timestamp gets a C++ int parameter, while the variable passed to it is an int64_t. The call narrows the value without any message. A date-window helper fed from input.time() therefore compares time against wrapped dates, and the script places no orders. The same comparison written inline trades normally.

In Pine, int holds millisecond timestamps (time, input.time() values, timestamp() results), so the narrowing changes results silently.

Versions

pineforge-codegen 1.9.1 and 1.9.0 emit the same code (1.6.1 too). The run below used pineforge-engine v1.9.0 (ec44968), Apple clang, macOS arm64. The narrowing is in the generated C++, so it should not depend on the platform.

Repro

//@version=6
strategy('repro')
start = input.time(timestamp('2020-01-01T00:00:00'), 'Start')
stop = input.time(timestamp('2034-07-01T00:00:00'), 'Stop')
inWindow(s, e) => time >= s and time <= e
if inWindow(start, stop) and bar_index % 20 == 0
    strategy.entry('L', strategy.long)
if bar_index % 20 == 10
    strategy.close('L')

Generated:

int64_t start = 0;
int64_t stop = 0;
// ...
bool inWindow(int s, int e) {

At the call, 1577836800000 (2020-01-01) becomes 1583802368 and 2035324800000 (2034-07-01) becomes -489698304, so time <= e is never true. On a daily series from 2020-01-01 to 2024-01-01 the script places no orders. Writing if time >= start and time <= stop and bar_index % 20 == 0 instead gives 66 trades on the same bars.

The parameter type seems to follow the argument rather than the variable:

Call Emitted signature
inWindow(start, stop) with input.time() values bool inWindow(int s, int e)
f(a) => a called as f(1577836800000) int f(int a)
f(int a) => ... or f(simple int a) => ... with an input.time() value int parameter
f(t) => t called as f(time) int64_t f(int64_t t)

Expected

Integer parameters and return values of user functions keep 64 bits (int64_t), as Pine's int does; or, failing that, the narrowing is rejected with an error instead of passing silently.

Activity

  1. letiep97 commented on Nov 1, 2026

    @letiep97
    Author

    Three more places where a Pine int becomes a 32-bit C++ int, found on 1.9.1 and unchanged on main c1cc532. Built with pineforge-engine v1.9.1 (fc0a2df) on BTCUSDT 1h bars, 2024-01-01 to 2024-03-01 (1,441 bars); ok read back with // @pf-trace.

    A typed int parameter of a method. The untyped form already gets a double.

    //@version=6
    strategy('repro')
    type Stamp
        int t = 0
    method store(Stamp self, int value) =>
        self.t := value
    var s = Stamp.new()
    s.store(time)
    ok = s.t == time ? 1 : 0
    // @pf-trace ok=ok
    plot(ok)
    

    Generated: int _udt_Stamp_store(Stamp self, int value), while the field is int64_t t. ok is 0 on all 1,441 bars. With method store(Stamp self, value) the signature is (Stamp self, double value) and ok is 1 on every bar.

    An array<int> field of a UDT. A top-level array.new<int>() is already std::vector<int64_t>, and storing time in it gives ok 1.

    //@version=6
    strategy('repro')
    type Book
        array<int> stamps
    var b = Book.new(array.new<int>())
    b.stamps.unshift(time)
    ok = b.stamps.get(0) == time ? 1 : 0
    // @pf-trace ok=ok
    plot(ok)
    

    Generated: std::vector<int> stamps and .stamps = std::vector<int>(). ok is 0 on every bar. With array<float> it is 1.

    array.from() with an input.time() value. Here the narrowing is caught by the compiler instead of passing silently.

    //@version=6
    strategy('repro')
    start = input.time(timestamp('2024-01-01T00:00:00'), 'Start')
    var stamps = array.from(start)
    ok = stamps.get(0) == start ? 1 : 0
    // @pf-trace ok=ok
    plot(ok)
    

    Generated: std::vector<int> stamps; and stamps = std::vector<int>{start};, where start is int64_t. clang: non-constant-expression cannot be narrowed from type 'int64_t' (aka 'long long') to 'int' in initializer list. With array.from(float(start)) it builds and ok is 1.

    Expected, as for the cases in the issue: int64_t in all three, since a Pine int holds millisecond UNIX timestamps such as time and input.time() values (time), and ok is 1 on every bar. Checked on TradingView (BINANCE:BTCUSDT 1h): all three scripts compile and run, with no error or warning, and ok is 1 on every bar, as in their controls.

    Workarounds, in the same order: leave the method parameter untyped, use array<float> in the UDT, and wrap the value in float() for array.from().

  2. luisleo526 commented on Nov 1, 2026

    @luisleo526
    Contributor

    Thank you for the detailed report. We are checking each case against TradingView's behaviour and our source, and will post a result for each case in this issue by the end of 2026-11-03 (UTC). If a case needs longer, we will say so here with the reason.

  3. luisleo526 commented on Nov 3, 2026

    @luisleo526
    Contributor

    Fixed in #322, which is merged to main and ships in 1.10.0.

    What changed: time values now keep 64 bits through user-function parameters (typed, untyped and simple), typed integer method parameters, array<int> fields of user-defined types, and array.from(...) with a time input, including their copies, tuples and history reads. Several of these shapes stopped the C++ build before (the compiler refused the narrowing) and now compile and run. A missing integer value passed into a method parameter stays missing.

    Checks that ran: 54 behaviour tests run each case and compare the stored value with a reference computed in the same script; a comparison of 110 recorded TradingView scripts shows 65 now equal where the previous version differed, 20 equal on both, and none worse than before. TradingView's own integer is a 64-bit floating-point value above 2^53, so values above that are not promised to be exact integers; the target here is "no 32-bit narrowing".

    Known limits (separate defects, not changed here; they are also listed in the PR): two script shapes that stopped with a build failure before now build and run but print a value that differs from TradingView. A script that keeps an alias of an array past its source prints 2 where TradingView prints 3, and a copy of a slice that still shares elements prints 2 where TradingView prints 1 (0 of 26 compared bars equal in both). We are working on both separately.

    Thank you for the report.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions