Skip to content

Commit 395eea6

Browse files
authored
Merge pull request #196 from LuxAlgo/dev
## [0.9.14] - 2026-05-05 - UDT & Transpiler Hardening, `request.security`, Streaming & Drawing `na`
2 parents 562e721 + 489189a commit 395eea6

36 files changed

Lines changed: 3928 additions & 193 deletions

CHANGELOG.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,34 @@
11
# Change Log
22

3+
## [0.9.14] - 2026-05-05 - UDT & Transpiler Hardening, `request.security`, Streaming & Drawing `na`
4+
5+
### Added
6+
7+
- **`str.format_time(time, format, timezone)`**: Full string-based time formatting aligned with Pine Script (`Str.ts`, tests in `str.test.ts`).
8+
- **`for...of` runtime helpers**: Codegen uses **`$.iter`** / **`$.entries`** instead of brittle one-off special cases.
9+
- **UDT registry pre-pass**: **`preProcessUdtRegistry`** runs before analysis so **`ScopeManager`** is populated consistently (5 dedicated tests).
10+
11+
### Fixed
12+
13+
- **Live streaming with `eDate`**: **`runLive`** / live mode now works when an end date is provided; previously the combination was incorrectly rejected or behaved as non-live (**`PineTS.class.ts`**).
14+
- **`for...in` over `MemberExpression` iterables**: Destructuring in **`for...in`** when the iterable is a member expression (e.g. chained property access) is transformed correctly (**`MainTransformer`**).
15+
- **`request.security`**: Named arguments resolve through **`parseArgsForPineParams`** with **array/tuple-aware** `remaining_options`; secondary-context **bar alignment** improved; **`calc_bars_count`** threaded through the security pipeline.
16+
- **Callable drawing/table namespaces**: **`box`**, **`linefill`**, **`polyline`**, **`table`** — fixes for correct call vs instance dispatch in transpiled code.
17+
- **Reserved words in generated JS**: Transpiler avoids invalid identifiers when Pine names collide with JavaScript reserved words.
18+
- **Comma-separated typed declarations**: Multiple declarations on one line with shared type; guard tightened so **`chart.point[] a = ..., chart.point[] b = ...`** (dotted / repeated types) is split handled as separate statements instead of mis-parsing (**`parseTypedVarDeclaration`**).
19+
- **Contextual Pine Script keywords**: Parser treats keywords contextually so valid identifiers / constructs are not broken by overly greedy keyword rules.
20+
- **UDT field subscripts in call arguments**: Per-bar lookback (`seriesVar.field[1]`) inside function-call arguments is transformed correctly.
21+
- **UDT field-subscript & method transpiler**: Broader fixes for member access and methods on UDT instances.
22+
- **UDT `.new()` mixed positional + named args**: When the last argument is a plain object whose keys match UDT field names, it is stripped and applied as named fields instead of shifting positional slots (**`Core.ts` UDT constructor**).
23+
- **UFCS `obj.method()`**: Method names that are JS reserved words keep correct **`$M_`** naming without double **`_$N`** renames; **`Holder r = arr.get(0)`** with an explicit UDT type registers **`r`** for instance dispatch; **`.delete()`** on built-in drawing objects is not retargeted as a user method.
24+
- **Typed `na` for drawings**: **`box(na)`**, **`line(na)`**, **`label(na)`**, **`polyline(na)`**, **`linefill(na)`**, **`table(na)`** behave as Pine **typed `na`** / casts, not as calls that build empty drawing instances.
25+
- **UDT registration across function parameters**: Exiting a function that took a UDT parameter no longer clears outer-scope UDT-instance bindings — prior registrations are **snapshotted and restored** so patterns like **`var T x = T.new(...)`** plus **`foo(T x)`** still resolve **`x.foo()`** later.
26+
- **Plots & deleted drawings**: Deleted drawing objects are **omitted** from generated plot payloads so consumers do not see stale handles.
27+
- **Default colors**: Restored sensible default stroke/fill styling for **`box`** and **`polyline`** when colors are omitted.
28+
- **Documentation**: Various doc updates (merged with this release train).
29+
30+
---
31+
332
## [0.9.12] - 2026-04-15 - FMP Provider: `mintick`, Forex vs Crypto & Resilient `getSymbolInfo`
433

534
### Added

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "pinets",
3-
"version": "0.9.13",
3+
"version": "0.9.14",
44
"description": "Run Pine Script anywhere. PineTS is an open-source transpiler and runtime that brings Pine Script logic to Node.js and the browser with 1:1 syntax compatibility. Reliably write, port, and run indicators or strategies on your own infrastructure.",
55
"keywords": [
66
"Pine Script",

src/Context.class.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -736,6 +736,37 @@ export class Context {
736736
}
737737
}
738738

739+
/**
740+
* Resolve an iterable for `for x in collection` codegen.
741+
* Handles PineArrayObject (unwrap to inner JS array) and plain JS arrays uniformly.
742+
* Returns the value itself if it's already iterable (Map, Set, etc.).
743+
*
744+
* Centralizing this here means the transpiler can emit a uniform shape regardless of
745+
* whether the iterable is a built-in returning a plain array (e.g. box.all) or a UDT
746+
* field holding a PineArrayObject — and future collection types only need to update
747+
* this helper, not the codegen.
748+
*/
749+
iter(source: any): any {
750+
if (source == null) return [];
751+
// PineArrayObject wraps the underlying JS array as `.array`
752+
if (Array.isArray(source.array)) return source.array;
753+
return source;
754+
}
755+
756+
/**
757+
* Resolve an iterable yielding [index, value] tuples for `for [i, x] in collection`
758+
* destructuring codegen. PineArrayObject's [Symbol.iterator] yields scalar values, so
759+
* we must explicitly call `.entries()` on the underlying array.
760+
*/
761+
entries(source: any): IterableIterator<[number, any]> {
762+
if (source == null) return [].entries();
763+
if (Array.isArray(source.array)) return source.array.entries();
764+
if (Array.isArray(source)) return source.entries();
765+
// Map / Set / other iterables that already yield tuples
766+
if (typeof source.entries === 'function') return source.entries();
767+
return [].entries();
768+
}
769+
739770
//#region [Call Stack Management] ===========================
740771

741772
private _callStack: string[] = [];

src/namespaces/Core.ts

Lines changed: 36 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,13 @@ export class NAHelper {
8181
}
8282

8383
any(series: any) {
84+
// Pine Script function defaults like `param = na` get transpiled to JS
85+
// `param = na`, where `na` is this NAHelper instance. When the caller
86+
// omits the argument, the parameter ends up holding the helper itself
87+
// — which must be recognised as NA, not as a regular object.
88+
if (series instanceof NAHelper) return true;
8489
const val = Series.from(series).get(0);
90+
if (val instanceof NAHelper) return true;
8591
// null/undefined are always na
8692
if (val === null || val === undefined) return true;
8793
// For numbers, check NaN (Pine Script na for numeric types)
@@ -191,7 +197,15 @@ export class Core {
191197
return _options;
192198
}
193199
indicator(...args) {
194-
const options = parseIndicatorOptions(args);
200+
// The transpiler wraps every positional arg with `$.param(...)`, which
201+
// promotes booleans / numbers to a `Series` instance (strings and
202+
// objects pass through as-is). Multi-signature matching in
203+
// `parseArgsForPineParams` then fails the `boolean` / `number` type
204+
// checks because a Series is neither — so `overlay=true`,
205+
// `precision=N`, etc. silently drop back to defaults. Unwrap any
206+
// Series here to expose the underlying scalar.
207+
const unwrapped = args.map(a => a instanceof Series ? a.get(0) : a);
208+
const options = parseIndicatorOptions(unwrapped);
195209

196210
const defaults = {
197211
title: '',
@@ -494,23 +508,28 @@ export class Core {
494508
// Map positional args to field names, applying defaults for missing args
495509
const mappedArgs: Record<string, any> = {};
496510

497-
// Detect named args object: if the first (and only) argument is a plain
498-
// object whose keys match field names, treat it as named arguments.
499-
// This handles the Pine pattern: MyType.new(field1 = val1, field2 = val2)
500-
// which the transpiler converts to: MyType.new({ field1: val1, field2: val2 })
511+
// Detect a trailing named-args object. The transpiler turns
512+
// MyType.new(p1, p2, named1 = v1, named2 = v2)
513+
// into
514+
// MyType.new(p1, p2, { named1: v1, named2: v2 })
515+
// The named-args object is always the LAST positional, so check
516+
// args[args.length - 1] — not args[0]. Pure-named calls (length 1)
517+
// and mixed positional+named calls are both handled by this rule.
501518
let namedArgs: Record<string, any> | null = null;
502-
if (
503-
args.length === 1 &&
504-
args[0] &&
505-
typeof args[0] === 'object' &&
506-
!(args[0] instanceof Series) &&
507-
!Array.isArray(args[0]) &&
508-
!(args[0] instanceof PineTypeObject)
509-
) {
510-
const keys = Object.keys(args[0]);
511-
if (keys.length > 0 && keys.some((k) => definitionKeys.includes(k))) {
512-
namedArgs = args[0];
513-
args = []; // Clear positional args
519+
if (args.length > 0) {
520+
const last = args[args.length - 1];
521+
if (
522+
last &&
523+
typeof last === 'object' &&
524+
!(last instanceof Series) &&
525+
!Array.isArray(last) &&
526+
!(last instanceof PineTypeObject)
527+
) {
528+
const keys = Object.keys(last);
529+
if (keys.length > 0 && keys.some((k) => definitionKeys.includes(k))) {
530+
namedArgs = last;
531+
args = args.slice(0, -1);
532+
}
514533
}
515534
}
516535

src/namespaces/Str.ts

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,14 @@
33
import { Series } from '../Series';
44
import { Context } from '..';
55
import { PineArrayObject, PineArrayType } from './array/PineArrayObject';
6+
import { getDatePartsInTimezone } from './Time';
7+
8+
const MONTH_SHORT = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
9+
const MONTH_LONG = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
10+
const DAY_SHORT = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
11+
const DAY_LONG = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
12+
13+
const pad = (n: number, len: number) => String(n).padStart(len, '0');
614

715
export class Str {
816
constructor(private context: Context) {}
@@ -133,6 +141,109 @@ export class Str {
133141
return String(source).substring(begin_pos, end_pos);
134142
}
135143

144+
/**
145+
* Format a UNIX millisecond timestamp using Java SimpleDateFormat-style tokens
146+
* (yyyy, MM, dd, HH, mm, ss, EEE, EEEE, MMM, MMMM, a, h, S, Z, etc.).
147+
* Text inside single quotes is treated as a literal; '' produces a literal '.
148+
*/
149+
format_time(time: any, format: string = "yyyy-MM-dd'T'HH:mm:ssZ", timezone?: string) {
150+
if (time === null || time === undefined || (typeof time === 'number' && isNaN(time))) {
151+
return 'NaN';
152+
}
153+
const ts = Number(time);
154+
const tz = timezone || this.context.pine?.syminfo?.timezone || 'UTC';
155+
const parts = getDatePartsInTimezone(ts, tz);
156+
157+
// Compute timezone offset (for Z token) by comparing tz-local recomposed UTC ms to actual ts
158+
const tzAsUtc = Date.UTC(parts.year, parts.month - 1, parts.day, parts.hour, parts.minute, parts.second);
159+
const offsetMin = Math.round((tzAsUtc - ts) / 60000);
160+
161+
// Day of year (in target tz)
162+
const startOfYearUtc = Date.UTC(parts.year, 0, 1);
163+
const dayOfYear = Math.floor((tzAsUtc - startOfYearUtc) / 86400000) + 1;
164+
165+
const hour12 = parts.hour % 12 === 0 ? 12 : parts.hour % 12;
166+
167+
let result = '';
168+
let i = 0;
169+
while (i < format.length) {
170+
const ch = format[i];
171+
172+
// Single-quoted literal
173+
if (ch === "'") {
174+
if (format[i + 1] === "'") { result += "'"; i += 2; continue; }
175+
const end = format.indexOf("'", i + 1);
176+
if (end === -1) { result += format.substring(i + 1); break; }
177+
result += format.substring(i + 1, end);
178+
i = end + 1;
179+
continue;
180+
}
181+
182+
// Pattern token: count consecutive same-letter chars
183+
if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')) {
184+
let count = 1;
185+
while (format[i + count] === ch) count++;
186+
i += count;
187+
188+
switch (ch) {
189+
case 'y':
190+
result += count === 2 ? pad(parts.year % 100, 2) : count >= 4 ? pad(parts.year, 4) : String(parts.year);
191+
break;
192+
case 'M':
193+
if (count >= 4) result += MONTH_LONG[parts.month - 1];
194+
else if (count === 3) result += MONTH_SHORT[parts.month - 1];
195+
else if (count === 2) result += pad(parts.month, 2);
196+
else result += String(parts.month);
197+
break;
198+
case 'd':
199+
result += count === 2 ? pad(parts.day, 2) : String(parts.day);
200+
break;
201+
case 'D':
202+
result += count >= 3 ? pad(dayOfYear, 3) : count === 2 ? pad(dayOfYear, 2) : String(dayOfYear);
203+
break;
204+
case 'E':
205+
result += count >= 4 ? DAY_LONG[parts.dayOfWeek] : DAY_SHORT[parts.dayOfWeek];
206+
break;
207+
case 'a':
208+
result += parts.hour < 12 ? 'AM' : 'PM';
209+
break;
210+
case 'h':
211+
result += count === 2 ? pad(hour12, 2) : String(hour12);
212+
break;
213+
case 'H':
214+
result += count === 2 ? pad(parts.hour, 2) : String(parts.hour);
215+
break;
216+
case 'm':
217+
result += count === 2 ? pad(parts.minute, 2) : String(parts.minute);
218+
break;
219+
case 's':
220+
result += count === 2 ? pad(parts.second, 2) : String(parts.second);
221+
break;
222+
case 'S': {
223+
const ms = ts - Math.floor(ts / 1000) * 1000;
224+
result += pad(ms, 3).substring(0, count);
225+
break;
226+
}
227+
case 'Z': {
228+
const sign = offsetMin >= 0 ? '+' : '-';
229+
const absMin = Math.abs(offsetMin);
230+
result += `${sign}${pad(Math.floor(absMin / 60), 2)}${pad(absMin % 60, 2)}`;
231+
break;
232+
}
233+
default:
234+
// Unknown letter token — leave as-is
235+
result += ch.repeat(count);
236+
}
237+
continue;
238+
}
239+
240+
// Literal char
241+
result += ch;
242+
i++;
243+
}
244+
return result;
245+
}
246+
136247
format(message: string, ...args: any[]) {
137248
// Handle both simple {0} and extended {0,number,#.##} patterns
138249
return message.replace(/\{(\d+)(?:,number,([^}]+))?\}/g, (match, index, fmt) => {

src/namespaces/box/BoxHelper.ts

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export class BoxHelper {
4545
public syncToPlot() {
4646
this._ensurePlotsEntry();
4747
const time = this.context.marketData[0]?.openTime || 0;
48-
const allPlotData = this._boxes.map(bx => bx.toPlotData());
48+
const allPlotData = this._boxes.filter(bx => !bx._deleted).map(bx => bx.toPlotData());
4949

5050
// Split force_overlay objects into a separate overlay plot (renders on main chart pane)
5151
const regular = allPlotData.filter((b: any) => !b.force_overlay);
@@ -91,13 +91,15 @@ export class BoxHelper {
9191
}
9292

9393
/**
94-
* Resolve a color value, preserving NaN (na) so renderers can detect "no color".
95-
* The regular `_resolve(val) || fallback` pattern treats NaN as falsy and replaces
96-
* it with the default, losing the explicit `border_color = na` intent.
94+
* Resolve a color value, preserving na markers so renderers can detect "no color".
95+
* Pine emits na either as NaN (from `bgcolor = na`) or as null (from
96+
* `bgcolor = color(na)` — `color(na)` returns null per PineColor.any). Both
97+
* must survive — replacing them with a default would force renderers to paint
98+
* a visible color where the script asked for none.
9799
*/
98100
private _resolveColor(val: any, fallback: string): any {
99101
const resolved = this._resolve(val);
100-
// NaN means `na` in Pine Script — preserve it so renderers can detect it
102+
if (resolved === null || resolved === undefined) return resolved;
101103
if (typeof resolved === 'number' && isNaN(resolved)) return NaN;
102104
return resolved || fallback;
103105
}
@@ -202,7 +204,25 @@ export class BoxHelper {
202204
);
203205
}
204206

205-
any(...args: any[]): BoxObject {
207+
any(...args: any[]): BoxObject | null {
208+
// Pine `box(arg)` is a type cast / typed-na, NOT a constructor:
209+
// box bx = box(na) → typed-na (na(bx) is true)
210+
// box bx = box(some_box) → no-op cast (bx === some_box)
211+
// The constructor is `box.new(...)`. Multi-arg calls fall through to
212+
// .new() to preserve any incidental usage.
213+
if (args.length === 1) {
214+
const arg = args[0];
215+
if (arg === null || arg === undefined) return null;
216+
if (arg instanceof NAHelper) return null;
217+
if (typeof arg === 'number' && isNaN(arg)) return null;
218+
if (arg instanceof BoxObject) return arg;
219+
if (arg instanceof Series) {
220+
const v = arg.get(0);
221+
if (v === null || v === undefined || (typeof v === 'number' && isNaN(v))) return null;
222+
if (v instanceof BoxObject) return v;
223+
}
224+
return null;
225+
}
206226
return this.new(...args);
207227
}
208228

src/namespaces/label/LabelHelper.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export class LabelHelper {
4444
public syncToPlot() {
4545
this._ensurePlotsEntry();
4646
const time = this.context.marketData[0]?.openTime || 0;
47-
const allPlotData = this._labels.map(lbl => lbl.toPlotData());
47+
const allPlotData = this._labels.filter(lbl => !lbl._deleted).map(lbl => lbl.toPlotData());
4848

4949
// Split force_overlay objects into a separate overlay plot (renders on main chart pane)
5050
const regular = allPlotData.filter((l: any) => !l.force_overlay);
@@ -184,7 +184,21 @@ export class LabelHelper {
184184
}
185185

186186
// label() direct call — mapped via NAMESPACES_LIKE → label.any()
187-
any(...args: any[]): LabelObject {
187+
// Pine `label(arg)` is a type cast / typed-na, NOT a constructor.
188+
any(...args: any[]): LabelObject | null {
189+
if (args.length === 1) {
190+
const arg = args[0];
191+
if (arg === null || arg === undefined) return null;
192+
if (arg instanceof NAHelper) return null;
193+
if (typeof arg === 'number' && isNaN(arg)) return null;
194+
if (arg instanceof LabelObject) return arg;
195+
if (arg instanceof Series) {
196+
const v = arg.get(0);
197+
if (v === null || v === undefined || (typeof v === 'number' && isNaN(v))) return null;
198+
if (v instanceof LabelObject) return v;
199+
}
200+
return null;
201+
}
188202
return this.new(...args);
189203
}
190204

0 commit comments

Comments
 (0)