From cad00e713202bcfc9e5d739acdbc818d4d6b45f5 Mon Sep 17 00:00:00 2001 From: alaa-eddine Date: Thu, 5 Mar 2026 23:01:45 +0100 Subject: [PATCH 1/3] Fix UDT member functions access Fix for typed arrays handling Fix Box/Line instances member functions access --- src/Context.class.ts | 15 + src/namespaces/array/utils.ts | 3 +- src/namespaces/box/BoxHelper.ts | 2 + src/namespaces/box/BoxObject.ts | 32 + src/namespaces/label/LabelHelper.ts | 2 + src/namespaces/label/LabelObject.ts | 23 + src/namespaces/line/LineHelper.ts | 2 + src/namespaces/line/LineObject.ts | 24 + src/transpiler/pineToJS/parser.ts | 22 +- src/transpiler/settings.ts | 1 - .../transformers/StatementTransformer.ts | 8 +- tests/core/udt-drawing-objects.test.ts | 547 ++++++++++++++++++ tests/transpiler/parser-fixes.test.ts | 155 +++++ 13 files changed, 831 insertions(+), 5 deletions(-) create mode 100644 tests/core/udt-drawing-objects.test.ts diff --git a/src/Context.class.ts b/src/Context.class.ts index 0f157d2..c43ff89 100644 --- a/src/Context.class.ts +++ b/src/Context.class.ts @@ -12,6 +12,7 @@ import { Input } from './namespaces/input/input.index'; import PineMath from './namespaces/math/math.index'; import { PineRequest } from './namespaces/request/request.index'; import TechnicalAnalysis from './namespaces/ta/ta.index'; +import { PineTypeObject } from './namespaces/PineTypeObject'; import { Series } from './Series'; import { Log } from './namespaces/Log'; import { Str } from './namespaces/Str'; @@ -535,6 +536,20 @@ export class Context { src = src(); } + // Resolve thunks inside PineTypeObject fields (UDT instances). + // When a `var` declaration initializes a UDT with factory calls like + // `MyType.new(line.new(...), label.new(...))`, the factory calls are + // wrapped in thunks to prevent orphan objects on bars 1+. Here on bar 0, + // we evaluate those thunks to get the actual drawing objects. + if (src instanceof PineTypeObject) { + const def = src.__def__; + for (const key in def) { + if (typeof src[key] === 'function') { + src[key] = src[key](); + } + } + } + // First bar: Initialize with source value let value; if (src instanceof Series) { diff --git a/src/namespaces/array/utils.ts b/src/namespaces/array/utils.ts index 9830b4d..8f4d8d0 100644 --- a/src/namespaces/array/utils.ts +++ b/src/namespaces/array/utils.ts @@ -33,7 +33,8 @@ export function inferValueType(value: any): PineArrayType { } else if (typeof value === 'boolean') { return PineArrayType.bool; } else { - throw new Error('Cannot infer type from value'); + // Objects (LineObject, LabelObject, BoxObject, etc.) get 'any' type + return PineArrayType.any; } } diff --git a/src/namespaces/box/BoxHelper.ts b/src/namespaces/box/BoxHelper.ts index 3fe135a..e71086b 100644 --- a/src/namespaces/box/BoxHelper.ts +++ b/src/namespaces/box/BoxHelper.ts @@ -99,6 +99,7 @@ export class BoxHelper { this._resolve(text_formatting) || 'format_none', force_overlay, ); + b._helper = this; this._boxes.push(b); this._syncToPlot(); return b; @@ -289,6 +290,7 @@ export class BoxHelper { copy(id: BoxObject): BoxObject | undefined { if (!id) return undefined; const b = id.copy(); + b._helper = this; this._boxes.push(b); this._syncToPlot(); return b; diff --git a/src/namespaces/box/BoxObject.ts b/src/namespaces/box/BoxObject.ts index 84d4cfe..033cad3 100644 --- a/src/namespaces/box/BoxObject.ts +++ b/src/namespaces/box/BoxObject.ts @@ -34,6 +34,7 @@ export class BoxObject { // Flags public force_overlay: boolean; public _deleted: boolean; + public _helper: any; constructor( left: number, @@ -77,8 +78,39 @@ export class BoxObject { this.text_formatting = text_formatting; this.force_overlay = force_overlay; this._deleted = false; + this._helper = null; } + // --- Delegate methods for method-call syntax (e.g. myBox.set_right(x)) --- + + set_left(left: number): void { if (this._helper) this._helper.set_left(this, left); else if (!this._deleted) this.left = left; } + set_right(right: number): void { if (this._helper) this._helper.set_right(this, right); else if (!this._deleted) this.right = right; } + set_top(top: number): void { if (this._helper) this._helper.set_top(this, top); else if (!this._deleted) this.top = top; } + set_bottom(bottom: number): void { if (this._helper) this._helper.set_bottom(this, bottom); else if (!this._deleted) this.bottom = bottom; } + set_lefttop(left: number, top: number): void { if (this._helper) this._helper.set_lefttop(this, left, top); else if (!this._deleted) { this.left = left; this.top = top; } } + set_rightbottom(right: number, bottom: number): void { if (this._helper) this._helper.set_rightbottom(this, right, bottom); else if (!this._deleted) { this.right = right; this.bottom = bottom; } } + set_top_left_point(point: any): void { if (this._helper) this._helper.set_top_left_point(this, point); } + set_bottom_right_point(point: any): void { if (this._helper) this._helper.set_bottom_right_point(this, point); } + set_xloc(left: number, right: number, xloc: string): void { if (this._helper) this._helper.set_xloc(this, left, right, xloc); else if (!this._deleted) { this.left = left; this.right = right; this.xloc = xloc; } } + set_bgcolor(color: string): void { if (this._helper) this._helper.set_bgcolor(this, color); else if (!this._deleted) this.bgcolor = color; } + set_border_color(color: string): void { if (this._helper) this._helper.set_border_color(this, color); else if (!this._deleted) this.border_color = color; } + set_border_width(width: number): void { if (this._helper) this._helper.set_border_width(this, width); else if (!this._deleted) this.border_width = width; } + set_border_style(style: string): void { if (this._helper) this._helper.set_border_style(this, style); else if (!this._deleted) this.border_style = style; } + set_extend(extend: string): void { if (this._helper) this._helper.set_extend(this, extend); else if (!this._deleted) this.extend = extend; } + set_text(text: string): void { if (this._helper) this._helper.set_text(this, text); else if (!this._deleted) this.text = text; } + set_text_color(color: string): void { if (this._helper) this._helper.set_text_color(this, color); else if (!this._deleted) this.text_color = color; } + set_text_size(size: string): void { if (this._helper) this._helper.set_text_size(this, size); else if (!this._deleted) this.text_size = size; } + set_text_halign(align: string): void { if (this._helper) this._helper.set_text_halign(this, align); else if (!this._deleted) this.text_halign = align; } + set_text_valign(align: string): void { if (this._helper) this._helper.set_text_valign(this, align); else if (!this._deleted) this.text_valign = align; } + set_text_wrap(wrap: string): void { if (this._helper) this._helper.set_text_wrap(this, wrap); else if (!this._deleted) this.text_wrap = wrap; } + set_text_font_family(family: string): void { if (this._helper) this._helper.set_text_font_family(this, family); else if (!this._deleted) this.text_font_family = family; } + set_text_formatting(formatting: string): void { if (this._helper) this._helper.set_text_formatting(this, formatting); else if (!this._deleted) this.text_formatting = formatting; } + + get_left(): number { return this.left; } + get_right(): number { return this.right; } + get_top(): number { return this.top; } + get_bottom(): number { return this.bottom; } + delete(): void { this._deleted = true; } diff --git a/src/namespaces/label/LabelHelper.ts b/src/namespaces/label/LabelHelper.ts index 8d5056e..5480f4c 100644 --- a/src/namespaces/label/LabelHelper.ts +++ b/src/namespaces/label/LabelHelper.ts @@ -104,6 +104,7 @@ export class LabelHelper { this._resolve(text_font_family), force_overlay, ); + lbl._helper = this; this._labels.push(lbl); this._syncToPlot(); return lbl; @@ -235,6 +236,7 @@ export class LabelHelper { copy(id: LabelObject): LabelObject | undefined { if (!id) return undefined; const lbl = id.copy(); + lbl._helper = this; this._labels.push(lbl); this._syncToPlot(); return lbl; diff --git a/src/namespaces/label/LabelObject.ts b/src/namespaces/label/LabelObject.ts index 92d81d0..7ea929a 100644 --- a/src/namespaces/label/LabelObject.ts +++ b/src/namespaces/label/LabelObject.ts @@ -22,6 +22,7 @@ export class LabelObject { public text_font_family: string; public force_overlay: boolean; public _deleted: boolean; + public _helper: any; constructor( x: number, @@ -53,8 +54,30 @@ export class LabelObject { this.text_font_family = text_font_family; this.force_overlay = force_overlay; this._deleted = false; + this._helper = null; } + // --- Delegate methods for method-call syntax (e.g. myLabel.set_x(x)) --- + + set_x(x: number): void { if (this._helper) this._helper.set_x(this, x); else if (!this._deleted) this.x = x; } + set_y(y: number): void { if (this._helper) this._helper.set_y(this, y); else if (!this._deleted) this.y = y; } + set_xy(x: number, y: number): void { if (this._helper) this._helper.set_xy(this, x, y); else if (!this._deleted) { this.x = x; this.y = y; } } + set_text(text: string): void { if (this._helper) this._helper.set_text(this, text); else if (!this._deleted) this.text = text; } + set_color(color: string): void { if (this._helper) this._helper.set_color(this, color); else if (!this._deleted) this.color = color; } + set_textcolor(textcolor: string): void { if (this._helper) this._helper.set_textcolor(this, textcolor); else if (!this._deleted) this.textcolor = textcolor; } + set_size(size: string): void { if (this._helper) this._helper.set_size(this, size); else if (!this._deleted) this.size = size; } + set_style(style: string): void { if (this._helper) this._helper.set_style(this, style); else if (!this._deleted) this.style = style; } + set_textalign(textalign: string): void { if (this._helper) this._helper.set_textalign(this, textalign); else if (!this._deleted) this.textalign = textalign; } + set_tooltip(tooltip: string): void { if (this._helper) this._helper.set_tooltip(this, tooltip); else if (!this._deleted) this.tooltip = tooltip; } + set_xloc(xloc: string): void { if (this._helper) this._helper.set_xloc(this, xloc); else if (!this._deleted) this.xloc = xloc; } + set_yloc(yloc: string): void { if (this._helper) this._helper.set_yloc(this, yloc); else if (!this._deleted) this.yloc = yloc; } + set_point(point: any): void { if (this._helper) this._helper.set_point(this, point); } + set_text_font_family(family: string): void { if (!this._deleted) this.text_font_family = family; } + + get_x(): number { return this.x; } + get_y(): number { return this.y; } + get_text(): string { return this.text; } + delete(): void { this._deleted = true; } diff --git a/src/namespaces/line/LineHelper.ts b/src/namespaces/line/LineHelper.ts index a4bc489..4839318 100644 --- a/src/namespaces/line/LineHelper.ts +++ b/src/namespaces/line/LineHelper.ts @@ -101,6 +101,7 @@ export class LineHelper { this._resolve(width) || 1, force_overlay, ); + ln._helper = this; this._lines.push(ln); this._syncToPlot(); return ln; @@ -260,6 +261,7 @@ export class LineHelper { copy(id: LineObject): LineObject | undefined { if (!id) return undefined; const ln = id.copy(); + ln._helper = this; this._lines.push(ln); this._syncToPlot(); return ln; diff --git a/src/namespaces/line/LineObject.ts b/src/namespaces/line/LineObject.ts index 2d173e6..011ab02 100644 --- a/src/namespaces/line/LineObject.ts +++ b/src/namespaces/line/LineObject.ts @@ -19,6 +19,7 @@ export class LineObject { public width: number; public force_overlay: boolean; public _deleted: boolean; + public _helper: any; constructor( x1: number, @@ -44,8 +45,31 @@ export class LineObject { this.width = width; this.force_overlay = force_overlay; this._deleted = false; + this._helper = null; } + // --- Delegate methods for method-call syntax (e.g. myLine.set_x2(x)) --- + + set_x1(x: number): void { if (this._helper) this._helper.set_x1(this, x); else if (!this._deleted) this.x1 = x; } + set_y1(y: number): void { if (this._helper) this._helper.set_y1(this, y); else if (!this._deleted) this.y1 = y; } + set_x2(x: number): void { if (this._helper) this._helper.set_x2(this, x); else if (!this._deleted) this.x2 = x; } + set_y2(y: number): void { if (this._helper) this._helper.set_y2(this, y); else if (!this._deleted) this.y2 = y; } + set_xy1(x: number, y: number): void { if (this._helper) this._helper.set_xy1(this, x, y); else if (!this._deleted) { this.x1 = x; this.y1 = y; } } + set_xy2(x: number, y: number): void { if (this._helper) this._helper.set_xy2(this, x, y); else if (!this._deleted) { this.x2 = x; this.y2 = y; } } + set_color(color: string): void { if (this._helper) this._helper.set_color(this, color); else if (!this._deleted) this.color = color; } + set_width(width: number): void { if (this._helper) this._helper.set_width(this, width); else if (!this._deleted) this.width = width; } + set_style(style: string): void { if (this._helper) this._helper.set_style(this, style); else if (!this._deleted) this.style = style; } + set_extend(extend: string): void { if (this._helper) this._helper.set_extend(this, extend); else if (!this._deleted) this.extend = extend; } + set_xloc(x1: number, x2: number, xloc: string): void { if (this._helper) this._helper.set_xloc(this, x1, x2, xloc); else if (!this._deleted) { this.x1 = x1; this.x2 = x2; this.xloc = xloc; } } + set_first_point(point: any): void { if (this._helper) this._helper.set_first_point(this, point); } + set_second_point(point: any): void { if (this._helper) this._helper.set_second_point(this, point); } + + get_x1(): number { return this.x1; } + get_y1(): number { return this.y1; } + get_x2(): number { return this.x2; } + get_y2(): number { return this.y2; } + get_price(x: number): number { if (this._helper) return this._helper.get_price(this, x); return NaN; } + delete(): void { this._deleted = true; } diff --git a/src/transpiler/pineToJS/parser.ts b/src/transpiler/pineToJS/parser.ts index 599b291..623875d 100644 --- a/src/transpiler/pineToJS/parser.ts +++ b/src/transpiler/pineToJS/parser.ts @@ -703,6 +703,15 @@ export class Parser { paramType = (paramType || '') + this.advance().value; } + // Handle generic type: array, map, etc. + if ( + this.peek().type === TokenType.IDENTIFIER && + this.peek(1).type === TokenType.OPERATOR && this.peek(1).value === '<' + ) { + const genericType = this.parseTypeExpression(); + paramType = paramType ? paramType + ' ' + genericType : genericType; + } + const paramName = this.expect(TokenType.IDENTIFIER).value; const param = new Identifier(paramName); if (paramType) param.varType = paramType; @@ -765,6 +774,15 @@ export class Parser { paramType = (paramType || '') + this.advance().value; } + // Handle generic type: array, map, etc. + if ( + this.peek().type === TokenType.IDENTIFIER && + this.peek(1).type === TokenType.OPERATOR && this.peek(1).value === '<' + ) { + const genericType = this.parseTypeExpression(); + paramType = paramType ? paramType + ' ' + genericType : genericType; + } + const paramName = this.expect(TokenType.IDENTIFIER).value; const param = new Identifier(paramName); if (paramType) param.varType = paramType; @@ -1269,7 +1287,7 @@ export class Parser { while (this.matchEx(TokenType.KEYWORD, 'and', true) || this.peekOperatorEx(['&&'])) { this.advance(); - this.skipNewlines(); + this.skipNewlines(true); const right = this.parseEquality(); left = new BinaryExpression('&&', left, right); } @@ -1295,7 +1313,7 @@ export class Parser { while (this.peekOperatorEx(['<', '>', '<=', '>='])) { const op = this.advance().value; - this.skipNewlines(); + this.skipNewlines(true); const right = this.parseAdditive(); left = new BinaryExpression(op, left, right); } diff --git a/src/transpiler/settings.ts b/src/transpiler/settings.ts index 2dd83ec..39a3c37 100644 --- a/src/transpiler/settings.ts +++ b/src/transpiler/settings.ts @@ -55,7 +55,6 @@ export const CONTEXT_PINE_VARS = [ 'map', 'matrix', 'log', - 'map', //types 'Type', //UDT 'bool', diff --git a/src/transpiler/transformers/StatementTransformer.ts b/src/transpiler/transformers/StatementTransformer.ts index 6a536ca..b9b3927 100644 --- a/src/transpiler/transformers/StatementTransformer.ts +++ b/src/transpiler/transformers/StatementTransformer.ts @@ -698,10 +698,16 @@ export function transformForStatement(node: any, scopeManager: ScopeManager, c: scopeManager.popScope(); } }, - MemberExpression(node: any) { + MemberExpression(node: any, state: ScopeManager, c: any) { scopeManager.pushScope('for'); transformMemberExpression(node, '', scopeManager); scopeManager.popScope(); + // If still a MemberExpression after transformation, recurse into the + // object so user variable identifiers (e.g. lineMatrix in + // lineMatrix.rows()) get transformed via the Identifier handler. + if (node.type === 'MemberExpression' && node.object) { + c(node.object, state); + } }, CallExpression(node: any, state: ScopeManager, c: any) { // Set parent on callee so transformMemberExpression knows it's already being called diff --git a/tests/core/udt-drawing-objects.test.ts b/tests/core/udt-drawing-objects.test.ts new file mode 100644 index 0000000..2d4f8af --- /dev/null +++ b/tests/core/udt-drawing-objects.test.ts @@ -0,0 +1,547 @@ +import { describe, it, expect } from 'vitest'; +import { PineTS, Provider } from 'index'; + +/** + * Tests for UDT (User-Defined Types) containing drawing object fields (line, label, box). + * + * These tests cover the fix for the "$.get(...).ln.set_xy1 is not a function" bug, + * where factory method thunks inside `var` UDT declarations were stored as raw + * functions instead of being evaluated to actual drawing objects. + * + * The fix resolves thunks inside PineTypeObject fields in initVar() on bar 0. + */ +describe('UDT with Drawing Objects', () => { + const makePineTS = () => + new PineTS(Provider.Mock, 'BTCUSDC', 'W', null, new Date('2019-01-01').getTime(), new Date('2019-03-01').getTime()); + + // ---------------------------------------------------------------- + // Core fix: var UDT with line field — thunk resolution in initVar + // ---------------------------------------------------------------- + + it('var UDT with line field: set_xy1/set_xy2 work (thunk resolution)', async () => { + const pineTS = makePineTS(); + + const code = ` +//@version=6 +indicator("UDT Line", overlay=true) +type MyObj + line ln + +var MyObj container = MyObj.new( + line.new(na, na, na, na, color=#ff0000, width=2) +) +container.ln.set_xy1(bar_index, high) +container.ln.set_xy2(bar_index, low) +plot(close) +`; + const { plots } = await pineTS.run(code); + + // Line should be created and accessible + expect(plots['__lines__']).toBeDefined(); + const lines = plots['__lines__'].data[0].value; + expect(Array.isArray(lines)).toBe(true); + // Only 1 line should exist (no orphans from thunks firing on every bar) + expect(lines.filter((l: any) => !l._deleted).length).toBe(1); + + const ln = lines[0]; + expect(ln.color).toBe('#ff0000'); + expect(ln.width).toBe(2); + // Coordinates should have been updated by set_xy1/set_xy2 + expect(typeof ln.x1).toBe('number'); + expect(typeof ln.y1).toBe('number'); + expect(typeof ln.x2).toBe('number'); + expect(typeof ln.y2).toBe('number'); + expect(ln.x1).not.toBeNaN(); + }); + + it('var UDT with label field: set_xy/set_text work (thunk resolution)', async () => { + const pineTS = makePineTS(); + + const code = ` +//@version=6 +indicator("UDT Label", overlay=true) +type MyObj + label lb + +var MyObj container = MyObj.new( + label.new(na, na, "", color=#5b9cf6, style=label.style_label_down, size=size.small) +) +container.lb.set_xy(bar_index, high) +container.lb.set_text("Price: " + str.tostring(close, "#.##")) +plot(close) +`; + const { plots } = await pineTS.run(code); + + expect(plots['__labels__']).toBeDefined(); + const labels = plots['__labels__'].data[0].value; + expect(Array.isArray(labels)).toBe(true); + expect(labels.filter((l: any) => !l._deleted).length).toBe(1); + + const lb = labels[0]; + expect(typeof lb.x).toBe('number'); + expect(lb.x).not.toBeNaN(); + expect(lb.text).toContain('Price:'); + }); + + it('var UDT with box field: set_lefttop/set_rightbottom work (thunk resolution)', async () => { + const pineTS = makePineTS(); + + const code = ` +//@version=6 +indicator("UDT Box", overlay=true) +type MyObj + box bx + +var MyObj container = MyObj.new( + box.new(na, na, na, na, border_color=#5b9cf6, bgcolor=#5b9cf610) +) +container.bx.set_lefttop(bar_index - 5, high) +container.bx.set_rightbottom(bar_index, low) +plot(close) +`; + const { plots } = await pineTS.run(code); + + expect(plots['__boxes__']).toBeDefined(); + const boxes = plots['__boxes__'].data[0].value; + expect(Array.isArray(boxes)).toBe(true); + expect(boxes.filter((b: any) => !b._deleted).length).toBe(1); + + const bx = boxes[0]; + expect(typeof bx.left).toBe('number'); + expect(typeof bx.top).toBe('number'); + expect(bx.border_color).toBe('#5b9cf6'); + }); + + // ---------------------------------------------------------------- + // Multi-field UDT: line + label + box in a single type + // ---------------------------------------------------------------- + + it('var UDT with line + label + box fields: all thunks resolved, no orphans', async () => { + const pineTS = makePineTS(); + + const code = ` +//@version=6 +indicator("UDT Multi-Field", overlay=true) +type ObjectContainer + line ln + label lb + box bx + +var ObjectContainer myContainer = ObjectContainer.new( + line.new(na, na, na, na, color=#5b9cf6, width=2), + label.new(na, na, "", color=#5b9cf6, style=label.style_label_down, size=size.small), + box.new(na, na, na, na, border_color=#5b9cf6) +) + +myContainer.ln.set_xy1(bar_index, high) +myContainer.ln.set_xy2(bar_index, low) +myContainer.lb.set_xy(bar_index, high) +myContainer.lb.set_text("Price: " + str.tostring(close, "#.##")) +myContainer.bx.set_lefttop(bar_index - 4, ta.highest(high, 5)) +myContainer.bx.set_rightbottom(bar_index, ta.lowest(low, 5)) +plot(close, "Price Plot", color=color.new(#5b9cf6, 50)) +`; + const { plots } = await pineTS.run(code); + + // All three drawing object types should exist + expect(plots['__lines__']).toBeDefined(); + expect(plots['__labels__']).toBeDefined(); + expect(plots['__boxes__']).toBeDefined(); + + // Exactly 1 of each — no orphan objects from thunks on bars 1+ + const lines = plots['__lines__'].data[0].value.filter((l: any) => !l._deleted); + const labels = plots['__labels__'].data[0].value.filter((l: any) => !l._deleted); + const boxes = plots['__boxes__'].data[0].value.filter((b: any) => !b._deleted); + expect(lines.length).toBe(1); + expect(labels.length).toBe(1); + expect(boxes.length).toBe(1); + + // Verify the objects have valid coordinates (were updated via delegate methods) + expect(lines[0].x1).not.toBeNaN(); + expect(lines[0].y1).not.toBeNaN(); + expect(lines[0].color).toBe('#5b9cf6'); + expect(labels[0].text).toContain('Price:'); + expect(typeof boxes[0].left).toBe('number'); + expect(typeof boxes[0].top).toBe('number'); + }); + + // ---------------------------------------------------------------- + // Delegate methods (instance.method() syntax) on UDT fields + // ---------------------------------------------------------------- + + it('delegate methods on UDT line field (instance.set_xy1 syntax)', async () => { + const pineTS = makePineTS(); + + const { result } = await pineTS.run((context) => { + const { Type, line, na, bar_index, high, low } = context.pine; + + const MyType = Type({ ln: 'line' }); + var container = MyType.new(line.new(0, 100, 10, 200)); + + // Use delegate syntax: container.ln.set_xy1(...) + container.ln.set_xy1(42, 500); + container.ln.set_xy2(99, 600); + + var x1 = container.ln.get_x1(); + var y1 = container.ln.get_y1(); + var x2 = container.ln.get_x2(); + var y2 = container.ln.get_y2(); + + return { x1, y1, x2, y2 }; + }); + + expect(result.x1[0]).toBe(42); + expect(result.y1[0]).toBe(500); + expect(result.x2[0]).toBe(99); + expect(result.y2[0]).toBe(600); + }); + + it('delegate methods on UDT label field (instance.set_text syntax)', async () => { + const pineTS = makePineTS(); + + const { result } = await pineTS.run((context) => { + const { Type, label } = context.pine; + + const MyType = Type({ lb: 'label' }); + var container = MyType.new(label.new(0, 100, 'initial')); + + container.lb.set_text('updated'); + container.lb.set_xy(42, 500); + + var text = container.lb.get_text(); + var x = container.lb.get_x(); + var y = container.lb.get_y(); + + return { text, x, y }; + }); + + expect(result.text[0]).toBe('updated'); + expect(result.x[0]).toBe(42); + expect(result.y[0]).toBe(500); + }); + + it('delegate methods on UDT box field (instance.set_lefttop syntax)', async () => { + const pineTS = makePineTS(); + + const { result } = await pineTS.run((context) => { + const { Type, box } = context.pine; + + const MyType = Type({ bx: 'box' }); + var container = MyType.new(box.new(0, 100, 10, 50)); + + container.bx.set_lefttop(42, 500); + container.bx.set_rightbottom(99, 200); + + var left = container.bx.get_left(); + var top = container.bx.get_top(); + var right = container.bx.get_right(); + var bottom = container.bx.get_bottom(); + + return { left, top, right, bottom }; + }); + + expect(result.left[0]).toBe(42); + expect(result.top[0]).toBe(500); + expect(result.right[0]).toBe(99); + expect(result.bottom[0]).toBe(200); + }); + + // ---------------------------------------------------------------- + // UDT field persistence with var across bars + // ---------------------------------------------------------------- + + it('var UDT persists drawing object across bars (same object mutated)', async () => { + const pineTS = makePineTS(); + + const code = ` +//@version=6 +indicator("UDT Persistence", overlay=true) +type MyObj + line ln + +var MyObj container = MyObj.new( + line.new(0, 100, 10, 200) +) +container.ln.set_x1(bar_index) +container.ln.set_y1(close) +plot(close) +`; + const { plots } = await pineTS.run(code); + + const lines = plots['__lines__'].data[0].value; + // Should still be 1 line (var persists, not recreated) + expect(lines.filter((l: any) => !l._deleted).length).toBe(1); + // The line's coordinates should reflect the last bar's values + const ln = lines[0]; + expect(ln.x1).not.toBe(0); // Updated from initial 0 + }); + + // ---------------------------------------------------------------- + // Non-var UDT with drawing objects (let — no thunk wrapping) + // ---------------------------------------------------------------- + + it('let UDT with line field works without thunks', async () => { + const pineTS = makePineTS(); + + const { result } = await pineTS.run((context) => { + const { Type, line } = context.pine; + + const MyType = Type({ ln: 'line' }); + let container = MyType.new(line.new(0, 100, 10, 200)); + + container.ln.set_xy1(42, 500); + var x1 = container.ln.get_x1(); + var y1 = container.ln.get_y1(); + + return { x1, y1 }; + }); + + expect(result.x1[0]).toBe(42); + expect(result.y1[0]).toBe(500); + }); +}); + +/** + * Tests for simple `var` line/label/box declarations (no UDT). + * Ensures that thunk wrapping + initVar deferred evaluation still works + * correctly after the UDT fix. + */ +describe('var Drawing Objects (non-UDT, regression)', () => { + const makePineTS = () => + new PineTS(Provider.Mock, 'BTCUSDC', 'W', null, new Date('2019-01-01').getTime(), new Date('2019-03-01').getTime()); + + it('var line creates exactly 1 line (no orphans)', async () => { + const pineTS = makePineTS(); + + const { plots } = await pineTS.run((context) => { + var myLine = line.new(0, 100, 10, 200, xloc.bar_index, 'none', '#ff0000'); + line.set_xy1(myLine, bar_index, high); + line.set_xy2(myLine, bar_index, low); + return {}; + }); + + const lines = plots['__lines__'].data[0].value; + expect(lines.filter((l: any) => !l._deleted).length).toBe(1); + }); + + it('var label creates exactly 1 label (no orphans)', async () => { + const pineTS = makePineTS(); + + const { plots } = await pineTS.run((context) => { + var myLabel = label.new(0, 100, 'test'); + label.set_xy(myLabel, bar_index, high); + return {}; + }); + + const labels = plots['__labels__'].data[0].value; + expect(labels.filter((l: any) => !l._deleted).length).toBe(1); + }); + + it('var box creates exactly 1 box (no orphans)', async () => { + const pineTS = makePineTS(); + + const { plots } = await pineTS.run((context) => { + var myBox = box.new(0, 100, 10, 50); + box.set_lefttop(myBox, bar_index - 5, high); + box.set_rightbottom(myBox, bar_index, low); + return {}; + }); + + const boxes = plots['__boxes__'].data[0].value; + expect(boxes.filter((b: any) => !b._deleted).length).toBe(1); + }); +}); + +/** + * Tests for map namespace — ensuring no conflict with native JS Map. + * + * Covers the fix for duplicate 'map' in CONTEXT_PINE_VARS which caused + * `const {map, map} = $.pine` (invalid destructuring). + */ +describe('Map Namespace (no native JS conflict)', () => { + const makePineTS = () => + new PineTS(Provider.Mock, 'BTCUSDC', 'W', null, new Date('2019-01-01').getTime(), new Date('2019-03-01').getTime()); + + it('map.new() and map operations work in Pine Script source', async () => { + const pineTS = makePineTS(); + + const code = ` +//@version=6 +indicator("Map Test") +var m = map.new() +if bar_index == 0 + m.put("a", 10.0) + m.put("b", 20.0) + m.put("c", 30.0) +plot(m.size(), "size") +plot(m.get("b"), "val_b") +plot(m.contains("a") ? 1 : 0, "has_a") +`; + const { plots } = await pineTS.run(code); + + const lastVal = (plotName: string) => { + const data = plots[plotName]?.data; + return data?.[data.length - 1]?.value; + }; + + expect(lastVal('size')).toBe(3); + expect(lastVal('val_b')).toBe(20); + expect(lastVal('has_a')).toBe(1); + }); + + it('map with drawing object values (map)', async () => { + const pineTS = makePineTS(); + + const code = ` +//@version=6 +indicator("Map Drawing Objects", overlay=true) +var m = map.new() +if bar_index == 0 + m.put(1, line.new(0, 100, 10, 200, color=#ff0000)) + m.put(2, line.new(0, 300, 10, 400, color=#00ff00)) +plot(m.size(), "size") +plot(m.contains(1) ? 1 : 0, "has_1") +`; + const { plots } = await pineTS.run(code); + + const lastVal = (plotName: string) => { + const data = plots[plotName]?.data; + return data?.[data.length - 1]?.value; + }; + + expect(lastVal('size')).toBe(2); + expect(lastVal('has_1')).toBe(1); + }); +}); + +/** + * Tests for UDT with drawing objects in Pine Script source code (string). + * These use the full transpiler pipeline end-to-end. + */ +describe('UDT Drawing Objects - Pine Script Source (E2E)', () => { + const makePineTS = () => + new PineTS(Provider.Mock, 'BTCUSDC', 'W', null, new Date('2019-01-01').getTime(), new Date('2019-03-01').getTime()); + + it('full UDT Object Management indicator (the original bug script)', async () => { + const pineTS = makePineTS(); + + const code = ` +//@version=6 +indicator("UDT Object Management", overlay = true) +color MAIN_COLOR = #5b9cf6 +type ObjectContainer + line ln + label lb + box bx + +var ObjectContainer myContainer = ObjectContainer.new( + line.new(na, na, na, na, color = MAIN_COLOR, width = 2), + label.new(na, na, "", color = MAIN_COLOR, style = label.style_label_down, size = size.small), + box.new(na, na, na, na, border_color = MAIN_COLOR, bgcolor = color.new(MAIN_COLOR, 90)) +) + +myContainer.ln.set_xy1(bar_index, high) +myContainer.ln.set_xy2(bar_index, low) +myContainer.lb.set_xy(bar_index, high) +myContainer.lb.set_text("Price: " + str.tostring(close, "#.##")) + +int lookback = 5 +float top = ta.highest(high, lookback) +float bottom = ta.lowest(low, lookback) +myContainer.bx.set_lefttop(bar_index - lookback + 1, top) +myContainer.bx.set_rightbottom(bar_index, bottom) +plot(close, "Price Plot", color = color.new(MAIN_COLOR, 50)) +`; + const { plots } = await pineTS.run(code); + + // All drawing types present + expect(plots['__lines__']).toBeDefined(); + expect(plots['__labels__']).toBeDefined(); + expect(plots['__boxes__']).toBeDefined(); + expect(plots['Price Plot']).toBeDefined(); + + // Exactly 1 of each — no orphans + const lines = plots['__lines__'].data[0].value.filter((l: any) => !l._deleted); + const labels = plots['__labels__'].data[0].value.filter((l: any) => !l._deleted); + const boxes = plots['__boxes__'].data[0].value.filter((b: any) => !b._deleted); + expect(lines.length).toBe(1); + expect(labels.length).toBe(1); + expect(boxes.length).toBe(1); + + // Verify line properties + expect(lines[0].color).toBe('#5b9cf6'); + expect(lines[0].width).toBe(2); + expect(lines[0].x1).not.toBeNaN(); + expect(lines[0].y1).not.toBeNaN(); + + // Verify label text was set + expect(labels[0].text).toContain('Price:'); + + // Verify box coordinates + expect(typeof boxes[0].left).toBe('number'); + expect(typeof boxes[0].top).toBe('number'); + expect(boxes[0].left).not.toBeNaN(); + expect(boxes[0].top).not.toBeNaN(); + }); + + it('UDT with var and let drawing objects coexisting', async () => { + const pineTS = makePineTS(); + + const code = ` +//@version=6 +indicator("UDT var+let", overlay=true) +type MyObj + line persistent_ln + label persistent_lb + +var MyObj container = MyObj.new( + line.new(na, na, na, na, color=#ff0000), + label.new(na, na, "", color=#0000ff) +) + +container.persistent_ln.set_xy1(bar_index, high) +container.persistent_ln.set_xy2(bar_index, low) +container.persistent_lb.set_xy(bar_index, high) +container.persistent_lb.set_text(str.tostring(bar_index)) +plot(close) +`; + const { plots } = await pineTS.run(code); + + const lines = plots['__lines__'].data[0].value.filter((l: any) => !l._deleted); + const labels = plots['__labels__'].data[0].value.filter((l: any) => !l._deleted); + + // var UDT: exactly 1 line, 1 label + expect(lines.length).toBe(1); + expect(labels.length).toBe(1); + expect(lines[0].color).toBe('#ff0000'); + }); + + it('UDT with mixed drawing and scalar fields', async () => { + const pineTS = makePineTS(); + + const code = ` +//@version=6 +indicator("UDT Mixed Fields", overlay=true) +type PriceLevel + float price = 0.0 + string name = "" + line ln + +var PriceLevel level = PriceLevel.new(100.0, "support", line.new(0, 100, 10, 100, color=#00ff00)) + +level.ln.set_xy1(0, level.price) +level.ln.set_xy2(bar_index, level.price) +plot(level.price, "level_price") +`; + const { plots } = await pineTS.run(code); + + expect(plots['__lines__']).toBeDefined(); + const lines = plots['__lines__'].data[0].value.filter((l: any) => !l._deleted); + expect(lines.length).toBe(1); + expect(lines[0].color).toBe('#00ff00'); + + // Scalar field should be accessible + expect(plots['level_price']).toBeDefined(); + expect(plots['level_price'].data[0].value).toBe(100); + }); +}); diff --git a/tests/transpiler/parser-fixes.test.ts b/tests/transpiler/parser-fixes.test.ts index 9ff0f18..58beb8b 100644 --- a/tests/transpiler/parser-fixes.test.ts +++ b/tests/transpiler/parser-fixes.test.ts @@ -1158,3 +1158,158 @@ plot(_sum, "Sum") expect(lastValue).toBe(25); }); }); + +// --------------------------------------------------------------------------- +// 13. Multi-Line Expression Continuation +// --------------------------------------------------------------------------- +describe('Parser Fix: Multi-Line Expression Continuation', () => { + it('should parse "and" at end of line with continuation', () => { + const code = ` +//@version=5 +indicator("And Continuation") + +a = close < open and + low < high +plot(a ? 1 : 0) +`; + const pine2js = pineToJS(code); + expect(pine2js.success).toBe(true); + expect(pine2js.code).toContain('&&'); + }); + + it('should parse "or" at end of line with continuation', () => { + const code = ` +//@version=5 +indicator("Or Continuation") + +b = close > open or + high > low +plot(b ? 1 : 0) +`; + const pine2js = pineToJS(code); + expect(pine2js.success).toBe(true); + expect(pine2js.code).toContain('||'); + }); + + it('should parse chained "and" across multiple lines', () => { + const code = ` +//@version=5 +indicator("Chained And") + +c = close > open and + high > low and + volume > 0 +plot(c ? 1 : 0) +`; + const pine2js = pineToJS(code); + expect(pine2js.success).toBe(true); + // Should produce two && operators + const matches = pine2js.code!.match(/&&/g); + expect(matches).not.toBeNull(); + expect(matches!.length).toBeGreaterThanOrEqual(2); + }); + + it('should parse comparison operator at end of line with continuation', () => { + const code = ` +//@version=5 +indicator("Comparison Continuation") + +d = close > + open +plot(d ? 1 : 0) +`; + const pine2js = pineToJS(code); + expect(pine2js.success).toBe(true); + expect(pine2js.code).toContain('>'); + }); + + it('should parse mixed "and"/"or" across lines', () => { + const code = ` +//@version=5 +indicator("Mixed And Or") + +e = close < open and + low < high or + volume > 0 +plot(e ? 1 : 0) +`; + const pine2js = pineToJS(code); + expect(pine2js.success).toBe(true); + expect(pine2js.code).toContain('&&'); + expect(pine2js.code).toContain('||'); + }); + + it('should parse deeply nested multiline with parentheses', () => { + const code = ` +//@version=5 +indicator("Nested Parens") + +f = (close > open and + high > low) or + (volume > 0 and + close > 100) +plot(f ? 1 : 0) +`; + const pine2js = pineToJS(code); + expect(pine2js.success).toBe(true); + expect(pine2js.code).toContain('&&'); + expect(pine2js.code).toContain('||'); + }); + + it('should parse "not" on continuation line after "and"', () => { + const code = ` +//@version=5 +indicator("Not Continuation") + +g = close > open and + not (low > high) +plot(g ? 1 : 0) +`; + const pine2js = pineToJS(code); + expect(pine2js.success).toBe(true); + expect(pine2js.code).toContain('&&'); + expect(pine2js.code).toContain('!'); + }); + + it('should run multiline "and" condition at runtime', async () => { + const pineTS = new PineTS(Provider.Mock, 'BTCUSDC', '60', null, new Date('2024-01-01').getTime(), new Date('2024-01-10').getTime()); + + const code = ` +//@version=5 +indicator("And Runtime") + +_a = close < open and + low < high +plot(_a ? 1 : 0, "A") +`; + const { plots } = await pineTS.run(code); + expect(plots['A']).toBeDefined(); + expect(plots['A'].data.length).toBeGreaterThan(0); + + // Every value should be 0 or 1 + for (const pt of plots['A'].data) { + expect([0, 1]).toContain(pt.value); + } + }); + + it('should run multiline comparison continuation at runtime', async () => { + const pineTS = new PineTS(Provider.Mock, 'BTCUSDC', '60', null, new Date('2024-01-01').getTime(), new Date('2024-01-10').getTime()); + + const code = ` +//@version=5 +indicator("Cmp Runtime") + +_d = close > + open +plot(_d ? 1 : 0, "D") +`; + const { plots } = await pineTS.run(code); + expect(plots['D']).toBeDefined(); + expect(plots['D'].data.length).toBeGreaterThan(0); + + // Every value should be 0 or 1 + for (const pt of plots['D'].data) { + expect([0, 1]).toContain(pt.value); + } + }); +}); From f4ea2c2045e78cbf28f0561ed32b7268024ab633 Mon Sep 17 00:00:00 2001 From: alaa-eddine Date: Fri, 6 Mar 2026 00:50:46 +0100 Subject: [PATCH 2/3] Fix : histogram histbase Fix : non computed properties access --- .../transformers/ExpressionTransformer.ts | 20 ++++++- src/types/PineTypes.ts | 2 +- tests/transpiler/parser-fixes.test.ts | 60 +++++++++++++++++++ 3 files changed, 79 insertions(+), 3 deletions(-) diff --git a/src/transpiler/transformers/ExpressionTransformer.ts b/src/transpiler/transformers/ExpressionTransformer.ts index 20af1ca..2ab9251 100644 --- a/src/transpiler/transformers/ExpressionTransformer.ts +++ b/src/transpiler/transformers/ExpressionTransformer.ts @@ -512,15 +512,24 @@ function transformOperand(node: any, scopeManager: ScopeManager, namespace: stri return getParamFromLogicalExpression(node, scopeManager, namespace); } case 'MemberExpression': { + // For non-computed property access on NAMESPACES_LIKE identifiers (e.g. label.style_label_down), + // leave as-is — these are namespace constant accesses, not series values. + const isNamespacePropAccess = !node.computed && + node.object.type === 'Identifier' && + NAMESPACES_LIKE.includes(node.object.name) && + scopeManager.isContextBound(node.object.name); + // Handle array access - const transformedObject = node.object.type === 'Identifier' ? transformIdentifierForParam(node.object, scopeManager) : node.object; + const transformedObject = (node.object.type === 'Identifier' && !isNamespacePropAccess) + ? transformIdentifierForParam(node.object, scopeManager) + : node.object; // For non-computed property access on user variables (e.g. get_spt.output), // wrap the object in $.get() to extract the current bar's value. // Without this, `$.let.glb1_get_spt.output` accesses the Series object itself, // not the current bar value's property. let finalObject = transformedObject; - if (!node.computed && node.object.type === 'Identifier') { + if (!node.computed && node.object.type === 'Identifier' && !isNamespacePropAccess) { const [scopedName] = scopeManager.getVariable(node.object.name); const isUserVariable = scopedName !== node.object.name; if (isUserVariable && !scopeManager.isLoopVariable(node.object.name)) { @@ -650,6 +659,13 @@ function getParamFromConditionalExpression(node: any, scopeManager: ScopeManager Identifier(node: any, state: any, c: any) { if (node.name == 'NaN') return; if (NAMESPACES_LIKE.includes(node.name) && scopeManager.isContextBound(node.name)) { + // Skip wrapping when this identifier is the object of a non-computed + // member access (e.g. label.style_label_down) — it's a namespace + // constant access, not a series value. + const isMemberAccess = state.parent && state.parent.type === 'MemberExpression' && + state.parent.object === node && !state.parent.computed; + if (isMemberAccess) return; + const originalName = node.name; const valueExpr = { type: 'MemberExpression', diff --git a/src/types/PineTypes.ts b/src/types/PineTypes.ts index 14208b9..72a4122 100644 --- a/src/types/PineTypes.ts +++ b/src/types/PineTypes.ts @@ -24,7 +24,7 @@ export type PlotOptions = { linewidth?: number; style?: string; trackprice?: boolean; - histbase?: boolean; + histbase?: number; offset?: number; join?: boolean; editable?: boolean; diff --git a/tests/transpiler/parser-fixes.test.ts b/tests/transpiler/parser-fixes.test.ts index 58beb8b..7722bfa 100644 --- a/tests/transpiler/parser-fixes.test.ts +++ b/tests/transpiler/parser-fixes.test.ts @@ -1313,3 +1313,63 @@ plot(_d ? 1 : 0, "D") } }); }); + +// --------------------------------------------------------------------------- +// 14. Namespace Constants in Ternary Arguments +// --------------------------------------------------------------------------- +describe('Parser Fix: Namespace Constants in Ternary Arguments', () => { + it('should not wrap namespace property access with $.get in ternary inside function args', () => { + const code = ` +//@version=5 +indicator("Label Style Ternary") + +_above = close > open +label.new(bar_index, close, "X", + style = _above ? label.style_label_down : label.style_label_up) +`; + const result = transpile(code); + const jsCode = result.toString(); + + // label.style_label_down should NOT be wrapped with $.get(label.__value, 0) + expect(jsCode).not.toContain('label.__value'); + // It should appear as direct namespace access + expect(jsCode).toContain('label.style_label_down'); + expect(jsCode).toContain('label.style_label_up'); + }); + + it('should run label with ternary style at runtime', async () => { + const pineTS = new PineTS(Provider.Mock, 'BTCUSDC', '60', null, new Date('2024-01-01').getTime(), new Date('2024-01-10').getTime()); + + const code = ` +//@version=5 +indicator("Label Style Runtime", overlay=true) + +_above = close > open +label.new(bar_index, close, "X", + style = _above ? label.style_label_down : label.style_label_up, + color = color.new(color.blue, 50)) +plot(close, "Close") +`; + // Should not throw "Cannot read properties of undefined (reading 'style_label_down')" + const { plots } = await pineTS.run(code); + expect(plots['Close']).toBeDefined(); + expect(plots['__labels__']).toBeDefined(); + }); + + it('should preserve line namespace constants in ternary args', () => { + const code = ` +//@version=5 +indicator("Line Style Ternary") + +_bull = close > open +line.new(bar_index[1], close[1], bar_index, close, + style = _bull ? line.style_solid : line.style_dashed) +`; + const result = transpile(code); + const jsCode = result.toString(); + + expect(jsCode).not.toContain('line.__value'); + expect(jsCode).toContain('line.style_solid'); + expect(jsCode).toContain('line.style_dashed'); + }); +}); From 06f0c94ef9a1b32d489b741fb9d5a6c3b5f5630e Mon Sep 17 00:00:00 2001 From: alaa-eddine Date: Fri, 6 Mar 2026 01:21:59 +0100 Subject: [PATCH 3/3] changelog --- CHANGELOG.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cab5d62..ee06e61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,28 @@ # Change Log +## [0.9.2] - 2026-03-06 - Drawing Object Method Syntax, Gradient Fill, Matrix & Array Improvements + +### Added + +- **Method-Call Syntax on Drawing Instances**: `LineObject`, `LabelObject`, and `BoxObject` now carry delegate setter/getter methods directly on the instance (e.g., `myLine.set_x2(x)`, `myBox.set_right(r)`, `myLabel.set_text(t)`). Each delegate forwards to the owning helper so the plot sync (`_syncToPlot`) fires correctly. Enables Pine Script patterns where drawing objects stored in UDTs or arrays are mutated via method syntax. +- **Gradient Fill (`fill()`)**: Added support for Pine Script's gradient fill signature — `fill(plot1, plot2, top_value, bottom_value, top_color, bottom_color)`. The `FillHelper` detects the gradient form (third argument is a number) and stores per-bar `top_value`/`bottom_value`/`top_color`/`bottom_color` data for the renderer. +- **Typed Generic Function Parameters**: The Pine Script parser now correctly handles generic type annotations in function parameter lists (e.g., `array src`, `map data`). Previously these caused parse errors. + +### Fixed + +- **UDT Thunk Resolution for Drawing Object Fields**: When a `var` UDT instance contains fields initialised with factory calls (e.g., `line.new(...)`, `box.new(...)`), those fields are now correctly resolved as thunks on bar 0 inside `initVar`. Previously the thunk-wrapped factory results were stored as raw functions in the UDT field, causing the drawing object to never be created. +- **Typed Array Type Inference for Object Types**: `inferValueType()` no longer throws `"Cannot infer type from value"` when called with an object (e.g., a `LineObject` or `BoxObject`). It now returns `PineArrayType.any`, allowing `array` and similar typed arrays to work correctly. +- **Non-Computed Namespace Property Access in `$.param()`**: Fixed `ExpressionTransformer` incorrectly wrapping namespace constant accesses (e.g., `label.style_label_down`, `line.style_dashed`) in `$.get()` calls when they appeared inside function arguments. The transformer now detects non-computed member access on `NAMESPACES_LIKE` identifiers and leaves them untransformed. +- **`histbase` Type in `PlotOptions`**: Fixed the `histbase` field in the `PlotOptions` TypeScript type from `boolean` to `number`, matching the actual Pine Script `plot(histbase=50)` signature. +- **For-Loop `MemberExpression` Recursion**: Fixed user variable identifiers inside method calls in `for` loops (e.g., `lineMatrix.rows()`) not being transformed. The `MemberExpression` visitor in `transformForStatement` now recurses into the object node after transformation so nested identifiers are correctly resolved. +- **Multiline `and` / Comparison Expressions**: Fixed the Pine Script parser dropping continuation lines in `and`/`&&` chains and comparison expressions spanning multiple lines. `skipNewlines(true)` is now called after the operator. +- **`matrix.inv()` — Full NxN Support**: Rewrote `matrix.inv()` from a 2×2-only implementation to Gauss-Jordan elimination with partial pivoting, supporting any square matrix. Singular matrices (pivot < 1e-14) return a NaN matrix. +- **`matrix.pinv()` — Real Pseudoinverse**: Rewrote `matrix.pinv()` from a placeholder stub to a correct Moore-Penrose pseudoinverse: square → `inv()`, tall (m > n) → `(AᵀA)⁻¹Aᵀ`, wide (m < n) → `Aᵀ(AAᵀ)⁻¹`. +- **`array.min()` / `array.max()` Performance**: Added an O(N) fast path for the common `nth=0` case instead of always sorting O(N log N). +- **`array.median()`, `percentile_linear_interpolation()`, `percentile_nearest_rank()` Performance**: Single-pass copy-and-validate optimizations. +- **`isPlot()` with Undefined Title**: Fixed `isPlot()` to accept plot objects that have `_plotKey` but no `title` property (e.g., fill plots created via callsite ID), preventing `fill()` from misidentifying its arguments (contribution by @dcaoyuan, [#142](https://github.com/QuantForgeOrg/PineTS/issues/142)). +- **Duplicate `map` in `CONTEXT_PINE_VARS`**: Removed an accidental duplicate `'map'` entry from `settings.ts`. + ## [0.9.1] - 2026-03-04 - Enum Values, ATR/DMI/Supertrend Fixes, UDT & Transpiler Improvements ### Added