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
275 changes: 144 additions & 131 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
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
7 changes: 5 additions & 2 deletions src/transpiler/transformers/ExpressionTransformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,8 +233,11 @@ export function transformIdentifier(node: any, scopeManager: ScopeManager): void

if (isContextBoundVar) {
const isFunctionArg = node.parent && node.parent.type === 'CallExpression' && node.parent.arguments.includes(node);
if (!isFunctionArg) {
// Return early if it's not a function arg that needs unwrapping
const isSwitchDiscriminant = node.parent && node.parent.type === 'SwitchStatement' && node.parent.discriminant === node;
const isSwitchCaseTest = node.parent && node.parent.type === 'SwitchCase' && node.parent.test === node;

if (!isFunctionArg && !isSwitchDiscriminant && !isSwitchCaseTest) {
// Return early if it's not a function arg or switch test that needs unwrapping
return;
}
}
Expand Down
Loading