11from __future__ import annotations
22
3+ from collections import Counter
34from dataclasses import dataclass , field
45
56from . import ast as AST
@@ -26,6 +27,7 @@ class Symbol:
2627 line : int
2728 column : int
2829 kind : str
30+ type_name : str | None = None
2931 used : bool = False
3032
3133
@@ -121,17 +123,32 @@ def collect_direct_declarations(self, statements: list[AST.Statement], scope: Sc
121123 if statement .alias :
122124 self .define_symbol (scope , statement .alias , statement .line , statement .column , "namespace" )
123125 elif isinstance (statement , AST .VariableDeclaration ):
124- self .define_symbol (scope , statement .name , statement .name_line , statement .name_column , "variable" )
126+ self .define_symbol (
127+ scope ,
128+ statement .name ,
129+ statement .name_line ,
130+ statement .name_column ,
131+ "variable" ,
132+ self .extract_declared_type_name (statement .type_annotation ),
133+ )
125134 elif isinstance (statement , AST .DestructuringAssignment ):
126135 for variable in statement .variables :
127136 self .define_symbol (scope , variable .name , variable .line , variable .column , "variable" )
128137
129- def define_symbol (self , scope : Scope , name : str , line : int , column : int , kind : str ) -> None :
138+ def define_symbol (
139+ self ,
140+ scope : Scope ,
141+ name : str ,
142+ line : int ,
143+ column : int ,
144+ kind : str ,
145+ type_name : str | None = None ,
146+ ) -> None :
130147 if name == "_" :
131148 return
132149 existing = scope .symbols .get (name )
133150 if existing is None :
134- scope .symbols [name ] = Symbol (name = name , line = line , column = column , kind = kind )
151+ scope .symbols [name ] = Symbol (name = name , line = line , column = column , kind = kind , type_name = type_name )
135152 return
136153
137154 if existing .kind == kind :
@@ -173,6 +190,8 @@ def validate_statement(self, statement: AST.Statement, scope: Scope, conditional
173190 self .validate_type_annotation (statement .type_annotation , scope , statement .line , statement .column )
174191 if statement .init is not None :
175192 self .validate_expression (statement .init , scope , conditional_context )
193+ if self .is_bool_type_annotation (statement .type_annotation ) and self .is_na_literal (statement .init ):
194+ self .bool_na_error (statement .init .line , statement .init .column )
176195 return
177196
178197 if isinstance (statement , AST .DestructuringAssignment ):
@@ -233,6 +252,8 @@ def validate_statement(self, statement: AST.Statement, scope: Scope, conditional
233252 if field .type_annotation is not None :
234253 self .validate_type_annotation (field .type_annotation , scope , field .line , field .column )
235254 if field .default_value is not None :
255+ if self .is_bool_type_annotation (field .type_annotation ) and self .is_na_literal (field .default_value ):
256+ self .bool_na_error (field .default_value .line , field .default_value .column )
236257 if not self .is_valid_type_field_default (field .default_value ):
237258 self .errors .append (
238259 Diagnostic (
@@ -258,15 +279,24 @@ def validate_statement(self, statement: AST.Statement, scope: Scope, conditional
258279 for param in statement .params :
259280 if param .type_annotation is not None :
260281 self .validate_type_annotation (param .type_annotation , scope , param .line , param .column )
261- self .define_symbol (function_scope , param .name , param .line , param .column , "parameter" )
282+ self .define_symbol (
283+ function_scope ,
284+ param .name ,
285+ param .line ,
286+ param .column ,
287+ "parameter" ,
288+ self .extract_declared_type_name (param .type_annotation ),
289+ )
262290 if param .default_value is not None :
263291 self .validate_expression (param .default_value , function_scope )
292+ if self .is_bool_type_annotation (param .type_annotation ) and self .is_na_literal (param .default_value ):
293+ self .bool_na_error (param .default_value .line , param .default_value .column )
264294 self .collect_direct_declarations (statement .body , function_scope )
265295 self .validate_block (statement .body , function_scope )
266296 return
267297
268298 if isinstance (statement , AST .IfStatement ):
269- self .validate_expression (statement .condition , scope , conditional_context )
299+ self .validate_boolean_condition_expression (statement .condition , scope , conditional_context )
270300 consequent_scope = Scope (parent = scope )
271301 self .collect_direct_declarations (statement .consequent , consequent_scope )
272302 self .validate_block (statement .consequent , consequent_scope , conditional_context or "scope" )
@@ -296,7 +326,7 @@ def validate_statement(self, statement: AST.Statement, scope: Scope, conditional
296326 return
297327
298328 if isinstance (statement , AST .WhileStatement ):
299- self .validate_expression (statement .condition , scope , conditional_context )
329+ self .validate_boolean_condition_expression (statement .condition , scope , conditional_context )
300330 loop_scope = Scope (parent = scope )
301331 self .collect_direct_declarations (statement .body , loop_scope )
302332 self .validate_block (statement .body , loop_scope , conditional_context )
@@ -400,14 +430,14 @@ def validate_expression(self, expression: AST.Expression, scope: Scope, conditio
400430 return
401431
402432 if isinstance (expression , AST .TernaryExpression ):
403- self .validate_expression (expression .condition , scope , conditional_context )
433+ self .validate_boolean_condition_expression (expression .condition , scope , conditional_context )
404434 ternary_context = conditional_context or "ternary"
405435 self .validate_expression (expression .consequent , scope , ternary_context )
406436 self .validate_expression (expression .alternate , scope , ternary_context )
407437 return
408438
409439 if isinstance (expression , AST .IfExpression ):
410- self .validate_expression (expression .condition , scope , conditional_context )
440+ self .validate_boolean_condition_expression (expression .condition , scope , conditional_context )
411441 ternary_context = conditional_context or "ternary"
412442 self .validate_expression (expression .consequent , scope , ternary_context )
413443 self .validate_expression (expression .alternate , scope , ternary_context )
@@ -617,7 +647,23 @@ def resolve_generic_function_name(self, function_name: str) -> str:
617647
618648 def validate_call_signature (self , call : AST .CallExpression , function_name : str , spec : FunctionSpec ) -> None :
619649 positional_args = [arg for arg in call .arguments if arg .name is None ]
620- provided_named = {arg .name for arg in call .arguments if arg .name is not None }
650+ named_arguments = [arg for arg in call .arguments if arg .name is not None ]
651+ duplicate_named = [name for name , count in Counter (arg .name for arg in named_arguments ).items () if count > 1 ]
652+ if duplicate_named :
653+ for name in sorted (duplicate_named ):
654+ self .errors .append (
655+ Diagnostic (
656+ line = call .line ,
657+ column = call .column ,
658+ length = len (name ),
659+ message = f'Function call cannot include repeated argument for parameter "{ name } "' ,
660+ severity = Severity .ERROR ,
661+ source = "ast" ,
662+ )
663+ )
664+ return
665+
666+ provided_named = {arg .name for arg in named_arguments }
621667 if function_name .startswith ("input." ):
622668 provided_named = {
623669 name
@@ -967,6 +1013,118 @@ def extract_integer_literal(self, expression: AST.Expression) -> tuple[int, str,
9671013 return signed_value , f"{ expression .operator } { raw_value } " , expression .line , expression .column
9681014 return None
9691015
1016+ def validate_boolean_condition_expression (
1017+ self ,
1018+ expression : AST .Expression ,
1019+ scope : Scope ,
1020+ conditional_context : str | None = None ,
1021+ ) -> None :
1022+ self .validate_expression (expression , scope , conditional_context )
1023+ inferred = self .infer_boolean_expression (expression , scope )
1024+ if inferred is False :
1025+ self .errors .append (
1026+ Diagnostic (
1027+ line = expression .line ,
1028+ column = expression .column ,
1029+ length = max (1 , len (self .describe_expression (expression ))),
1030+ message = 'Condition expression must be of type "bool" in Pine Script v6' ,
1031+ severity = Severity .ERROR ,
1032+ source = "ast" ,
1033+ )
1034+ )
1035+
1036+ def infer_boolean_expression (self , expression : AST .Expression , scope : Scope ) -> bool | None :
1037+ if isinstance (expression , AST .Literal ):
1038+ if isinstance (expression .value , bool ):
1039+ return True
1040+ return False
1041+
1042+ if isinstance (expression , AST .Identifier ):
1043+ symbol = self .lookup_accessible_symbol (scope , expression .name , expression .line , expression .column )
1044+ if symbol is not None and symbol .type_name == "bool" :
1045+ return True
1046+ if expression .name in self .builtins .standalone_variables :
1047+ return False
1048+ return None
1049+
1050+ if isinstance (expression , AST .MemberExpression ):
1051+ if isinstance (expression .object , AST .Identifier ):
1052+ if expression .object .name == "barstate" :
1053+ return True
1054+ symbol = self .lookup_accessible_symbol (scope , expression .object .name , expression .line , expression .column )
1055+ if symbol is not None and symbol .type_name == "bool" :
1056+ return True
1057+ return None
1058+
1059+ if isinstance (expression , AST .CallExpression ):
1060+ function_name = self .extract_function_name (expression .callee )
1061+ if function_name in {"ta.cross" , "ta.crossover" , "ta.crossunder" }:
1062+ return True
1063+ return None
1064+
1065+ if isinstance (expression , AST .UnaryExpression ):
1066+ if expression .operator == "not" :
1067+ return True
1068+ return False
1069+
1070+ if isinstance (expression , AST .BinaryExpression ):
1071+ if expression .operator in {"==" , "!=" , ">" , "<" , ">=" , "<=" , "and" , "or" }:
1072+ return True
1073+ return False
1074+
1075+ if isinstance (expression , AST .TernaryExpression ):
1076+ consequent = self .infer_boolean_expression (expression .consequent , scope )
1077+ alternate = self .infer_boolean_expression (expression .alternate , scope )
1078+ if consequent is True and alternate is True :
1079+ return True
1080+ if consequent is False or alternate is False :
1081+ return False
1082+ return None
1083+
1084+ if isinstance (expression , AST .IfExpression ):
1085+ consequent = self .infer_boolean_expression (expression .consequent , scope )
1086+ alternate = self .infer_boolean_expression (expression .alternate , scope )
1087+ if consequent is True and alternate is True :
1088+ return True
1089+ if consequent is False or alternate is False :
1090+ return False
1091+ return None
1092+
1093+ return None
1094+
1095+ @staticmethod
1096+ def extract_declared_type_name (type_annotation : AST .TypeAnnotation | None ) -> str | None :
1097+ if type_annotation is None :
1098+ return None
1099+ return type_annotation .name
1100+
1101+ def is_bool_type_annotation (self , type_annotation : AST .TypeAnnotation | None ) -> bool :
1102+ type_name = self .extract_declared_type_name (type_annotation )
1103+ if type_name is None :
1104+ return False
1105+ return self .strip_array_suffix (type_name ) == "bool"
1106+
1107+ @staticmethod
1108+ def is_na_literal (expression : AST .Expression ) -> bool :
1109+ return isinstance (expression , AST .Literal ) and expression .value == "na"
1110+
1111+ def describe_expression (self , expression : AST .Expression ) -> str :
1112+ if isinstance (expression , AST .Identifier ):
1113+ return expression .name
1114+ if isinstance (expression , AST .Literal ):
1115+ return expression .raw
1116+ if isinstance (expression , AST .MemberExpression ):
1117+ parent = self .describe_expression (expression .object )
1118+ return f"{ parent } .{ expression .property .name } "
1119+ if isinstance (expression , AST .CallExpression ):
1120+ function_name = self .extract_function_name (expression .callee )
1121+ return function_name or "call"
1122+ if isinstance (expression , AST .BinaryExpression ):
1123+ return expression .operator
1124+ if isinstance (expression , AST .UnaryExpression ):
1125+ return expression .operator
1126+ return "expression"
1127+
9701128 def mutable_argument_error (self , name : str , line : int , column : int ) -> None :
9711129 self .errors .append (
9721130 Diagnostic (
@@ -979,6 +1137,18 @@ def mutable_argument_error(self, name: str, line: int, column: int) -> None:
9791137 )
9801138 )
9811139
1140+ def bool_na_error (self , line : int , column : int ) -> None :
1141+ self .errors .append (
1142+ Diagnostic (
1143+ line = line ,
1144+ column = column ,
1145+ length = 2 ,
1146+ message = 'Cannot assign "na" to a "bool" value in Pine Script v6' ,
1147+ severity = Severity .ERROR ,
1148+ source = "ast" ,
1149+ )
1150+ )
1151+
9821152 def lookup_accessible_symbol (
9831153 self ,
9841154 scope : Scope ,
0 commit comments