Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Change Log

## [0.8.5] - 2026-01-27 - Transpiler Hotfixes

### Fixed
- **Deprecation Warnings**: Fixed wrong warning message appearing with valid code.
- **Pine Script Parser**: Fixed multiline Pine Script conditions parsing (indent error).
- **Transpiler**: Fixed `switch` statement syntax conversion.

## [0.8.4] - 2026-01-24 - Math Namespace Enhancements & Critical Fixes

### Added
Expand Down
275 changes: 144 additions & 131 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "pinets",
"version": "0.8.4",
"version": "0.8.5",
"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.",
"keywords": [
"Pine Script",
Expand Down Expand Up @@ -92,6 +92,7 @@
"rollup-plugin-esbuild": "^6.1.1",
"rollup-plugin-sourcemaps": "^0.6.3",
"rollup-plugin-typescript-paths": "^1.5.0",
"tsx": "^4.21.0",
"vite-tsconfig-paths": "^4.3.2",
"vitest": "^2.0.0"
},
Expand Down
2 changes: 1 addition & 1 deletion src/namespaces/ta/methods/hma.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export function hma(context: any) {
const sqrtPeriod = Math.floor(Math.sqrt(period));

// Get wma function from context.ta
const wmaFn = context.ta.wma;
const wmaFn = context.pine.ta.wma;

// Pass derived call IDs to internal WMA calls to avoid state collision
const wma1 = wmaFn(source, halfPeriod, _callId ? `${_callId}_wma1` : undefined);
Expand Down
8 changes: 4 additions & 4 deletions src/namespaces/ta/methods/macd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@ export function macd(context: any) {
const signalEmaId = `${baseId}_signal`;

// Calculate Fast and Slow EMAs
// context.ta.ema returns the current EMA value
const fastMA = context.ta.ema(source, fastLength, fastEmaId);
const slowMA = context.ta.ema(source, slowLength, slowEmaId);
// context.pine.ta.ema returns the current EMA value
const fastMA = context.pine.ta.ema(source, fastLength, fastEmaId);
const slowMA = context.pine.ta.ema(source, slowLength, slowEmaId);

// Calculate MACD Line
// Handle NaN cases if EMAs are not yet valid
Expand All @@ -55,7 +55,7 @@ export function macd(context: any) {
// We must ensure we don't pass NaN to EMA, as it might corrupt the state (initSum).
let signalLine = NaN;
if (!isNaN(macdLine)) {
signalLine = context.ta.ema(macdLine, signalLength, signalEmaId);
signalLine = context.pine.ta.ema(macdLine, signalLength, signalEmaId);
}

// Calculate Histogram
Expand Down
2 changes: 1 addition & 1 deletion src/namespaces/ta/methods/mom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ export function mom(context: any) {
const length = Series.from(_length).get(0);

// Momentum is same as change
return context.ta.change(source, length, _callId);
return context.pine.ta.change(source, length, _callId);
};
}
58 changes: 31 additions & 27 deletions src/transpiler/pineToJS/codegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1289,40 +1289,44 @@ export class CodeGenerator {
this.write('}');
}

// Generate SwitchExpression (convert to ternary chain)
// Generate SwitchExpression (convert to IIFE with switch statement)
generateSwitchExpression(node) {
// switch discriminant => chain of ternary operators
// switch x
// A => result1
// B => result2
// => defaultResult
// becomes: (x == A ? result1 : x == B ? result2 : defaultResult)
this.write('(');

for (let i = 0; i < node.cases.length; i++) {
const c = node.cases[i];

this.write('(() => {\n');
this.indent++;
this.write(this.indentStr.repeat(this.indent));

this.write('switch (');
this.generateExpression(node.discriminant);
this.write(') {\n');

this.indent++;

for (const c of node.cases) {
this.write(this.indentStr.repeat(this.indent));

if (c.test) {
// Compare discriminant to test value
this.generateExpression(node.discriminant);
this.write(' == ');
this.write('case ');
this.generateExpression(c.test);
this.write(' ? ');
this.generateExpression(c.consequent);
this.write(' : ');
this.write(':\n');
} else {
// Default case (no test) - just the consequent
this.generateExpression(c.consequent);
this.write('default:\n');
}

this.indent++;
this.write(this.indentStr.repeat(this.indent));
this.write('return ');
this.generateExpression(c.consequent);
this.write(';\n');
this.indent--;
}

this.indent--;
this.write(this.indentStr.repeat(this.indent));
this.write('}\n'); // end switch

// If no default case was provided, add undefined
const hasDefault = node.cases.some((c) => !c.test);
if (!hasDefault) {
this.write('undefined');
}

this.write(')');
this.indent--;
this.write(this.indentStr.repeat(this.indent));
this.write('})()');
}

// Generate SequenceExpression
Expand Down
2 changes: 1 addition & 1 deletion src/transpiler/pineToJS/lexer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ export class Lexer {

// Check if this is a blank line (only whitespace followed by newline or EOF)
// If so, skip indentation processing and keep position at whitespace
if (this.peek() === '\n' || this.peek() === '\0') {
if (this.peek() === '\n' || this.peek() === '\r' || this.peek() === '\0') {
// Don't process indentation for blank lines
// The whitespace will be skipped in the main loop
return;
Expand Down
114 changes: 91 additions & 23 deletions src/transpiler/pineToJS/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,45 @@ export class Parser {
return this.advance();
}

skipNewlines() {
// Match a token, optionally ignoring NEWLINE and INDENT (for line continuation)
matchEx(type, value = null, allowLineContinuation = false) {
if (!allowLineContinuation) {
return this.match(type, value);
}

let offset = 0;
let token = this.peek(offset);

// Skip NEWLINE and subsequent INDENT
if (token.type === TokenType.NEWLINE) {
offset++;
token = this.peek(offset);

// Optional INDENT after NEWLINE
if (token.type === TokenType.INDENT) {
offset++;
token = this.peek(offset);
}
}

if (token.type !== type) return false;
if (value !== null && token.value !== value) return false;

// Consume skipped tokens
for (let i = 0; i < offset; i++) {
this.advance();
}

return true;
}

skipNewlines(allowIndent = false) {
while (this.match(TokenType.NEWLINE)) {
this.advance();
}
if (allowIndent && this.match(TokenType.INDENT)) {
this.advance();
}
}

// Main parse method
Expand All @@ -84,6 +119,13 @@ export class Parser {

while (!this.match(TokenType.EOF)) {
this.skipNewlines();

// Handle DEDENTs at top level (from line continuations)
if (this.match(TokenType.DEDENT)) {
this.advance();
continue;
}

if (this.match(TokenType.EOF)) break;

const stmt = this.parseStatement();
Expand Down Expand Up @@ -170,7 +212,7 @@ export class Parser {
const op = this.peek().value;
if (['=', ':=', '+=', '-=', '*=', '/=', '%='].includes(op)) {
this.advance();
this.skipNewlines();
this.skipNewlines(true);
const right = this.parseExpression();

// Simple assignment with = creates variable declaration
Expand Down Expand Up @@ -344,7 +386,7 @@ export class Parser {
}

this.expect(TokenType.OPERATOR, '=');
this.skipNewlines();
this.skipNewlines(true);
const init = this.parseExpression();

const id = new Identifier(name);
Expand All @@ -366,7 +408,7 @@ export class Parser {

const name = this.expect(TokenType.IDENTIFIER).value;
this.expect(TokenType.OPERATOR, '=');
this.skipNewlines();
this.skipNewlines(true);
const init = this.parseExpression();

const id = new Identifier(name);
Expand Down Expand Up @@ -598,7 +640,7 @@ export class Parser {
const op = this.peek().value;
if (['=', ':=', '+=', '-=', '*=', '/=', '%='].includes(op)) {
this.advance();
this.skipNewlines();
this.skipNewlines(true);
const right = this.parseExpression();

// Simple assignment with = creates variable declaration
Expand Down Expand Up @@ -774,19 +816,38 @@ export class Parser {
return new BlockStatement(stmt ? [stmt] : []);
}

const blockIndent = this.peek().indent;
this.advance(); // consume INDENT

const statements = [];
while (!this.match(TokenType.DEDENT) && !this.match(TokenType.EOF)) {
while (!this.match(TokenType.EOF)) {
this.skipNewlines();
if (this.match(TokenType.DEDENT)) break;

// Check for DEDENT
if (this.match(TokenType.DEDENT)) {
const dedentLevel = this.peek().indent;
if (dedentLevel < blockIndent) {
// Dedenting out of this block
break;
} else {
// Dedenting from a deeper level back to this block (or deeper)
// Consume spurious DEDENT
this.advance();
continue;
}
}

if (this.match(TokenType.EOF)) break;

const stmt = this.parseStatement();
if (stmt) statements.push(stmt);
}

if (this.match(TokenType.DEDENT)) {
this.advance();
const dedentLevel = this.peek().indent;
if (dedentLevel < blockIndent) {
this.advance();
}
}

return new BlockStatement(statements);
Expand Down Expand Up @@ -846,7 +907,7 @@ export class Parser {
this.expect(TokenType.RBRACKET);
this.skipNewlines();
this.expect(TokenType.OPERATOR, '=');
this.skipNewlines();
this.skipNewlines(true);
const init = this.parseExpression();

return new VariableDeclaration([new VariableDeclarator(new ArrayPattern(elements), init)], VariableDeclarationKind.CONST);
Expand All @@ -860,12 +921,19 @@ export class Parser {
parseTernary() {
let expr = this.parseLogicalOr();

if (this.match(TokenType.OPERATOR, '?')) {
if (this.matchEx(TokenType.OPERATOR, '?', true)) {
this.advance();
this.skipNewlines();
this.skipNewlines(true);
const consequent = this.parseExpression();
this.expect(TokenType.COLON);
this.skipNewlines();

// Handle : with line continuation
if (this.matchEx(TokenType.COLON, null, true)) {
this.advance(); // Consume :
} else {
this.expect(TokenType.COLON);
}

this.skipNewlines(true);
const alternate = this.parseExpression();
return new ConditionalExpression(expr, consequent, alternate);
}
Expand All @@ -876,9 +944,9 @@ export class Parser {
parseLogicalOr() {
let left = this.parseLogicalAnd();

while (this.match(TokenType.KEYWORD, 'or') || (this.match(TokenType.OPERATOR) && this.peek().value === '||')) {
while (this.matchEx(TokenType.KEYWORD, 'or', true) || (this.matchEx(TokenType.OPERATOR, null, true) && this.peek().value === '||')) {
this.advance();
this.skipNewlines();
this.skipNewlines(true);
const right = this.parseLogicalAnd();
left = new BinaryExpression('||', left, right);
}
Expand All @@ -889,7 +957,7 @@ export class Parser {
parseLogicalAnd() {
let left = this.parseEquality();

while (this.match(TokenType.KEYWORD, 'and') || (this.match(TokenType.OPERATOR) && this.peek().value === '&&')) {
while (this.matchEx(TokenType.KEYWORD, 'and', true) || (this.matchEx(TokenType.OPERATOR, null, true) && this.peek().value === '&&')) {
this.advance();
this.skipNewlines();
const right = this.parseEquality();
Expand All @@ -902,12 +970,12 @@ export class Parser {
parseEquality() {
let left = this.parseComparison();

while (this.match(TokenType.OPERATOR)) {
while (this.matchEx(TokenType.OPERATOR, null, true)) {
const op = this.peek().value;
if (!['==', '!='].includes(op)) break;

this.advance();
this.skipNewlines();
this.skipNewlines(true);
const right = this.parseComparison();
left = new BinaryExpression(op, left, right);
}
Expand All @@ -918,7 +986,7 @@ export class Parser {
parseComparison() {
let left = this.parseAdditive();

while (this.match(TokenType.OPERATOR)) {
while (this.matchEx(TokenType.OPERATOR, null, true)) {
const op = this.peek().value;
if (!['<', '>', '<=', '>='].includes(op)) break;

Expand All @@ -934,12 +1002,12 @@ export class Parser {
parseAdditive() {
let left = this.parseMultiplicative();

while (this.match(TokenType.OPERATOR)) {
while (this.matchEx(TokenType.OPERATOR, null, true)) {
const op = this.peek().value;
if (!['+', '-'].includes(op)) break;

this.advance();
this.skipNewlines();
this.skipNewlines(true);
const right = this.parseMultiplicative();
left = new BinaryExpression(op, left, right);
}
Expand All @@ -950,12 +1018,12 @@ export class Parser {
parseMultiplicative() {
let left = this.parseUnary();

while (this.match(TokenType.OPERATOR)) {
while (this.matchEx(TokenType.OPERATOR, null, true)) {
const op = this.peek().value;
if (!['*', '/', '%'].includes(op)) break;

this.advance();
this.skipNewlines();
this.skipNewlines(true);
const right = this.parseUnary();
left = new BinaryExpression(op, left, right);
}
Expand Down
Loading