vinter 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/LICENSE +13 -0
- data/README.md +1 -5
- data/lib/vinter/lexer.rb +125 -32
- data/lib/vinter/parser.rb +913 -50
- data/lib/vinter.rb +1 -1
- metadata +4 -3
data/lib/vinter/parser.rb
CHANGED
@@ -93,6 +93,14 @@ module Vinter
|
|
93
93
|
when 'vim9script'
|
94
94
|
token = advance # Skip 'vim9script'
|
95
95
|
{ type: :vim9script_declaration, line: token[:line], column: token[:column] }
|
96
|
+
when 'autocmd'
|
97
|
+
parse_autocmd_statement
|
98
|
+
when 'execute'
|
99
|
+
parse_execute_statement
|
100
|
+
when 'let'
|
101
|
+
parse_let_statement
|
102
|
+
when 'echohl', 'echomsg'
|
103
|
+
parse_echo_statement
|
96
104
|
else
|
97
105
|
@warnings << {
|
98
106
|
message: "Unexpected keyword: #{current_token[:value]}",
|
@@ -104,7 +112,11 @@ module Vinter
|
|
104
112
|
nil
|
105
113
|
end
|
106
114
|
elsif current_token[:type] == :identifier
|
107
|
-
|
115
|
+
if current_token[:value] == "echo"
|
116
|
+
parse_echo_statement
|
117
|
+
else
|
118
|
+
parse_expression_statement
|
119
|
+
end
|
108
120
|
elsif current_token[:type] == :comment
|
109
121
|
parse_comment
|
110
122
|
else
|
@@ -119,6 +131,168 @@ module Vinter
|
|
119
131
|
end
|
120
132
|
end
|
121
133
|
|
134
|
+
def parse_execute_statement
|
135
|
+
token = advance # Skip 'execute'
|
136
|
+
line = token[:line]
|
137
|
+
column = token[:column]
|
138
|
+
|
139
|
+
# Parse arguments - typically string expressions with concatenation
|
140
|
+
# Just accept any tokens until we hit a statement terminator or another command
|
141
|
+
expressions = []
|
142
|
+
expr = parse_expression
|
143
|
+
expressions << expr if expr
|
144
|
+
|
145
|
+
# Return the execute statement
|
146
|
+
{
|
147
|
+
type: :execute_statement,
|
148
|
+
expressions: expressions,
|
149
|
+
line: line,
|
150
|
+
column: column
|
151
|
+
}
|
152
|
+
end
|
153
|
+
|
154
|
+
def parse_let_statement
|
155
|
+
token = advance # Skip 'let'
|
156
|
+
line = token[:line]
|
157
|
+
column = token[:column]
|
158
|
+
|
159
|
+
# Parse the target variable
|
160
|
+
target = nil
|
161
|
+
if current_token
|
162
|
+
case current_token[:type]
|
163
|
+
when :identifier, :global_variable, :script_local, :arg_variable, :option_variable, :special_variable
|
164
|
+
target = {
|
165
|
+
type: current_token[:type],
|
166
|
+
name: current_token[:value],
|
167
|
+
line: current_token[:line],
|
168
|
+
column: current_token[:column]
|
169
|
+
}
|
170
|
+
advance
|
171
|
+
else
|
172
|
+
@errors << {
|
173
|
+
message: "Expected variable name after let",
|
174
|
+
position: @position,
|
175
|
+
line: current_token ? current_token[:line] : 0,
|
176
|
+
column: current_token ? current_token[:column] : 0
|
177
|
+
}
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
# Skip the '=' or other assignment operator
|
182
|
+
operator = nil
|
183
|
+
if current_token && (
|
184
|
+
(current_token[:type] == :operator && current_token[:value] == '=') ||
|
185
|
+
current_token[:type] == :compound_operator)
|
186
|
+
operator = current_token[:value]
|
187
|
+
advance
|
188
|
+
else
|
189
|
+
@errors << {
|
190
|
+
message: "Expected assignment operator after variable in let statement",
|
191
|
+
position: @position,
|
192
|
+
line: current_token ? current_token[:line] : 0,
|
193
|
+
column: current_token ? current_token[:column] : 0
|
194
|
+
}
|
195
|
+
end
|
196
|
+
|
197
|
+
# Parse the value expression
|
198
|
+
value = parse_expression
|
199
|
+
|
200
|
+
{
|
201
|
+
type: :let_statement,
|
202
|
+
target: target,
|
203
|
+
operator: operator,
|
204
|
+
value: value,
|
205
|
+
line: line,
|
206
|
+
column: column
|
207
|
+
}
|
208
|
+
end
|
209
|
+
|
210
|
+
def parse_autocmd_statement
|
211
|
+
token = advance # Skip 'autocmd'
|
212
|
+
line = token[:line]
|
213
|
+
column = token[:column]
|
214
|
+
|
215
|
+
# Parse event name (like BufNewFile)
|
216
|
+
event = nil
|
217
|
+
if current_token && current_token[:type] == :identifier
|
218
|
+
event = advance[:value]
|
219
|
+
else
|
220
|
+
@errors << {
|
221
|
+
message: "Expected event name after 'autocmd'",
|
222
|
+
position: @position,
|
223
|
+
line: current_token ? current_token[:line] : 0,
|
224
|
+
column: current_token ? current_token[:column] : 0
|
225
|
+
}
|
226
|
+
end
|
227
|
+
|
228
|
+
# Parse pattern (like *.match)
|
229
|
+
pattern = nil
|
230
|
+
if current_token
|
231
|
+
pattern = current_token[:value]
|
232
|
+
advance
|
233
|
+
end
|
234
|
+
|
235
|
+
# Parse command (can be complex, including if statements)
|
236
|
+
commands = []
|
237
|
+
|
238
|
+
# Handle pipe-separated commands
|
239
|
+
in_command = true
|
240
|
+
while in_command && @position < @tokens.length
|
241
|
+
if current_token && current_token[:value] == '|'
|
242
|
+
advance # Skip '|'
|
243
|
+
end
|
244
|
+
|
245
|
+
# Parse the command
|
246
|
+
if current_token && current_token[:type] == :keyword
|
247
|
+
case current_token[:value]
|
248
|
+
when 'if'
|
249
|
+
commands << parse_if_statement
|
250
|
+
when 'echo'
|
251
|
+
commands << parse_echo_statement
|
252
|
+
# Add other command types as needed
|
253
|
+
else
|
254
|
+
# Generic command handling
|
255
|
+
cmd = parse_expression_statement
|
256
|
+
commands << cmd if cmd
|
257
|
+
end
|
258
|
+
elsif current_token && current_token[:type] == :identifier
|
259
|
+
cmd = parse_expression_statement
|
260
|
+
commands << cmd if cmd
|
261
|
+
else
|
262
|
+
in_command = false
|
263
|
+
end
|
264
|
+
|
265
|
+
# Check if we've reached the end of the autocmd command
|
266
|
+
if !current_token || current_token[:type] == :comment || current_token[:value] == "\n"
|
267
|
+
in_command = false
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
return {
|
272
|
+
type: :autocmd_statement,
|
273
|
+
event: event,
|
274
|
+
pattern: pattern,
|
275
|
+
commands: commands,
|
276
|
+
line: line,
|
277
|
+
column: column
|
278
|
+
}
|
279
|
+
end
|
280
|
+
|
281
|
+
def parse_echo_statement
|
282
|
+
token = advance #Skip 'echo'
|
283
|
+
line = token[:line]
|
284
|
+
column = token[:column]
|
285
|
+
|
286
|
+
expression = parse_expression
|
287
|
+
|
288
|
+
{
|
289
|
+
type: :echo_statement,
|
290
|
+
expression: expression,
|
291
|
+
line: line,
|
292
|
+
column: column
|
293
|
+
}
|
294
|
+
end
|
295
|
+
|
122
296
|
def parse_comment
|
123
297
|
comment = current_token[:value]
|
124
298
|
line = current_token[:line]
|
@@ -375,8 +549,6 @@ module Vinter
|
|
375
549
|
}
|
376
550
|
end
|
377
551
|
|
378
|
-
|
379
|
-
|
380
552
|
def parse_parameter_list
|
381
553
|
params = []
|
382
554
|
|
@@ -385,7 +557,8 @@ module Vinter
|
|
385
557
|
return params
|
386
558
|
end
|
387
559
|
|
388
|
-
|
560
|
+
# Parse parameters until we find a closing parenthesis
|
561
|
+
while @position < @tokens.length && current_token && current_token[:type] != :paren_close
|
389
562
|
# Check for variable args
|
390
563
|
if current_token && current_token[:type] == :ellipsis
|
391
564
|
ellipsis_token = advance
|
@@ -403,6 +576,17 @@ module Vinter
|
|
403
576
|
line: ellipsis_token[:line],
|
404
577
|
column: ellipsis_token[:column]
|
405
578
|
}
|
579
|
+
|
580
|
+
# After varargs, we expect closing paren
|
581
|
+
if current_token && current_token[:type] != :paren_close
|
582
|
+
@errors << {
|
583
|
+
message: "Expected closing parenthesis after varargs",
|
584
|
+
position: @position,
|
585
|
+
line: current_token[:line],
|
586
|
+
column: current_token[:column]
|
587
|
+
}
|
588
|
+
end
|
589
|
+
|
406
590
|
break
|
407
591
|
end
|
408
592
|
|
@@ -437,15 +621,23 @@ module Vinter
|
|
437
621
|
type: :parameter,
|
438
622
|
name: param_name[:value],
|
439
623
|
param_type: param_type,
|
440
|
-
optional:
|
624
|
+
optional: default_value != nil,
|
441
625
|
default_value: default_value,
|
442
626
|
line: param_name[:line],
|
443
627
|
column: param_name[:column]
|
444
628
|
}
|
445
629
|
|
630
|
+
# If we have a comma, advance past it and continue
|
446
631
|
if current_token && current_token[:type] == :comma
|
447
632
|
advance
|
448
|
-
|
633
|
+
# If we don't have a comma, we should have a closing paren
|
634
|
+
elsif current_token && current_token[:type] != :paren_close
|
635
|
+
@errors << {
|
636
|
+
message: "Expected comma or closing parenthesis after parameter",
|
637
|
+
position: @position,
|
638
|
+
line: current_token[:line],
|
639
|
+
column: current_token[:column]
|
640
|
+
}
|
449
641
|
break
|
450
642
|
end
|
451
643
|
end
|
@@ -548,6 +740,39 @@ module Vinter
|
|
548
740
|
end
|
549
741
|
|
550
742
|
def parse_expression_statement
|
743
|
+
# Check if this is an assignment using a compound operator
|
744
|
+
if current_token && current_token[:type] == :identifier
|
745
|
+
variable_name = current_token[:value]
|
746
|
+
variable_token = current_token
|
747
|
+
advance # Move past the identifier
|
748
|
+
|
749
|
+
if current_token && current_token[:type] == :compound_operator
|
750
|
+
operator = current_token[:value]
|
751
|
+
operator_token = current_token
|
752
|
+
advance # Move past the operator
|
753
|
+
|
754
|
+
right = parse_expression
|
755
|
+
|
756
|
+
return {
|
757
|
+
type: :compound_assignment,
|
758
|
+
operator: operator,
|
759
|
+
target: {
|
760
|
+
type: :identifier,
|
761
|
+
name: variable_name,
|
762
|
+
line: variable_token[:line],
|
763
|
+
column: variable_token[:column]
|
764
|
+
},
|
765
|
+
value: right,
|
766
|
+
line: operator_token[:line],
|
767
|
+
column: operator_token[:column]
|
768
|
+
}
|
769
|
+
end
|
770
|
+
|
771
|
+
# If it wasn't a compound assignment, backtrack
|
772
|
+
@position -= 1
|
773
|
+
end
|
774
|
+
|
775
|
+
# Regular expression statement handling
|
551
776
|
expr = parse_expression
|
552
777
|
{
|
553
778
|
type: :expression_statement,
|
@@ -562,24 +787,38 @@ module Vinter
|
|
562
787
|
end
|
563
788
|
|
564
789
|
def parse_binary_expression(precedence = 0)
|
565
|
-
left =
|
790
|
+
left = parse_unary_expression
|
566
791
|
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
571
|
-
op_precedence = operator_precedence(op)
|
792
|
+
# Handle multi-line expressions where operators may appear at line beginnings
|
793
|
+
while current_token &&
|
794
|
+
(current_token[:type] == :operator ||
|
795
|
+
(peek_token && peek_token[:type] == :operator && current_token[:type] == :whitespace))
|
572
796
|
|
573
|
-
|
797
|
+
# Skip any whitespace before the operator if it's at the beginning of a line
|
798
|
+
if current_token[:type] == :whitespace
|
799
|
+
advance
|
800
|
+
end
|
574
801
|
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
|
802
|
+
# Now we should be at the operator
|
803
|
+
if current_token && current_token[:type] == :operator &&
|
804
|
+
operator_precedence(current_token[:value]) >= precedence
|
805
|
+
op_token = advance
|
806
|
+
op = op_token[:value]
|
807
|
+
op_precedence = operator_precedence(op)
|
808
|
+
|
809
|
+
right = parse_binary_expression(op_precedence + 1)
|
810
|
+
|
811
|
+
left = {
|
812
|
+
type: :binary_expression,
|
813
|
+
operator: op,
|
814
|
+
left: left,
|
815
|
+
right: right,
|
816
|
+
line: op_token[:line],
|
817
|
+
column: op_token[:column]
|
818
|
+
}
|
819
|
+
else
|
820
|
+
break
|
821
|
+
end
|
583
822
|
end
|
584
823
|
|
585
824
|
return left
|
@@ -599,6 +838,10 @@ module Vinter
|
|
599
838
|
5
|
600
839
|
when '*', '/', '%' # Multiplication, division, modulo
|
601
840
|
6
|
841
|
+
when '.'
|
842
|
+
7
|
843
|
+
when '!'
|
844
|
+
8
|
602
845
|
else
|
603
846
|
0
|
604
847
|
end
|
@@ -611,10 +854,13 @@ module Vinter
|
|
611
854
|
line = token[:line]
|
612
855
|
column = token[:column]
|
613
856
|
|
857
|
+
# First parse the basic expression
|
858
|
+
expr = nil
|
859
|
+
|
614
860
|
case token[:type]
|
615
861
|
when :number
|
616
862
|
advance
|
617
|
-
{
|
863
|
+
expr = {
|
618
864
|
type: :literal,
|
619
865
|
value: token[:value],
|
620
866
|
token_type: :number,
|
@@ -623,14 +869,33 @@ module Vinter
|
|
623
869
|
}
|
624
870
|
when :string
|
625
871
|
advance
|
626
|
-
{
|
872
|
+
expr = {
|
627
873
|
type: :literal,
|
628
874
|
value: token[:value],
|
629
875
|
token_type: :string,
|
630
876
|
line: line,
|
631
877
|
column: column
|
632
878
|
}
|
633
|
-
when :
|
879
|
+
when :option_variable
|
880
|
+
# Handle Vim option variables (like &compatible)
|
881
|
+
advance
|
882
|
+
expr = {
|
883
|
+
type: :option_variable,
|
884
|
+
name: token[:value],
|
885
|
+
line: line,
|
886
|
+
column: column
|
887
|
+
}
|
888
|
+
when :special_variable
|
889
|
+
# Handle Vim special variables (like v:version)
|
890
|
+
advance
|
891
|
+
expr = {
|
892
|
+
type: :special_variable,
|
893
|
+
name: token[:value],
|
894
|
+
line: line,
|
895
|
+
column: column
|
896
|
+
}
|
897
|
+
when :script_local
|
898
|
+
# Handle script-local variables/functions (like s:var)
|
634
899
|
advance
|
635
900
|
|
636
901
|
# Check if this is a function call
|
@@ -638,17 +903,72 @@ module Vinter
|
|
638
903
|
return parse_function_call(token[:value], line, column)
|
639
904
|
end
|
640
905
|
|
641
|
-
{
|
642
|
-
type: :
|
906
|
+
expr = {
|
907
|
+
type: :script_local,
|
908
|
+
name: token[:value],
|
909
|
+
line: line,
|
910
|
+
column: column
|
911
|
+
}
|
912
|
+
when :global_variable
|
913
|
+
# Handle global variables (like g:var)
|
914
|
+
advance
|
915
|
+
expr = {
|
916
|
+
type: :global_variable,
|
643
917
|
name: token[:value],
|
644
918
|
line: line,
|
645
919
|
column: column
|
646
920
|
}
|
921
|
+
when :arg_variable
|
922
|
+
# Handle function argument variables (like a:var)
|
923
|
+
advance
|
924
|
+
expr = {
|
925
|
+
type: :arg_variable,
|
926
|
+
name: token[:value],
|
927
|
+
line: line,
|
928
|
+
column: column
|
929
|
+
}
|
930
|
+
when :identifier
|
931
|
+
advance
|
932
|
+
|
933
|
+
# Check if this is a function call
|
934
|
+
if current_token && current_token[:type] == :paren_open
|
935
|
+
return parse_function_call(token[:value], line, column)
|
936
|
+
end
|
937
|
+
|
938
|
+
# Special handling for execute command
|
939
|
+
if token[:value] == 'execute'
|
940
|
+
# Parse the string expressions for execute
|
941
|
+
# For now we'll just treat it as a normal identifier
|
942
|
+
expr = {
|
943
|
+
type: :identifier,
|
944
|
+
name: token[:value],
|
945
|
+
line: line,
|
946
|
+
column: column
|
947
|
+
}
|
948
|
+
else
|
949
|
+
expr = {
|
950
|
+
type: :identifier,
|
951
|
+
name: token[:value],
|
952
|
+
line: line,
|
953
|
+
column: column
|
954
|
+
}
|
955
|
+
end
|
647
956
|
when :paren_open
|
648
|
-
|
957
|
+
if is_lambda_expression
|
958
|
+
return parse_lambda_expression(line, column)
|
959
|
+
else
|
960
|
+
advance # Skip '('
|
961
|
+
expr = parse_expression
|
962
|
+
expect(:paren_close) # Expect and skip ')'
|
963
|
+
end
|
964
|
+
when :bracket_open
|
965
|
+
expr = parse_list_literal(line, column)
|
966
|
+
when :brace_open
|
967
|
+
expr = parse_dict_literal(line, column)
|
968
|
+
when :backslash
|
969
|
+
# Handle line continuation with backslash
|
970
|
+
advance
|
649
971
|
expr = parse_expression
|
650
|
-
expect(:paren_close) # Expect and skip ')'
|
651
|
-
return expr
|
652
972
|
else
|
653
973
|
@errors << {
|
654
974
|
message: "Unexpected token in expression: #{token[:type]}",
|
@@ -659,6 +979,471 @@ module Vinter
|
|
659
979
|
advance
|
660
980
|
return nil
|
661
981
|
end
|
982
|
+
|
983
|
+
# Now handle any chained property access or method calls
|
984
|
+
while current_token
|
985
|
+
# Check for property access with dot
|
986
|
+
if current_token[:type] == :operator && current_token[:value] == '.'
|
987
|
+
dot_token = advance # Skip '.'
|
988
|
+
|
989
|
+
# Next token should be an identifier (property name)
|
990
|
+
if !current_token || current_token[:type] != :identifier
|
991
|
+
@errors << {
|
992
|
+
message: "Expected property name after '.'",
|
993
|
+
position: @position,
|
994
|
+
line: current_token ? current_token[:line] : 0,
|
995
|
+
column: current_token ? current_token[:column] : 0
|
996
|
+
}
|
997
|
+
break
|
998
|
+
end
|
999
|
+
|
1000
|
+
property_token = advance # Get property name
|
1001
|
+
|
1002
|
+
expr = {
|
1003
|
+
type: :property_access,
|
1004
|
+
object: expr,
|
1005
|
+
property: property_token[:value],
|
1006
|
+
line: dot_token[:line],
|
1007
|
+
column: dot_token[:column]
|
1008
|
+
}
|
1009
|
+
# Check for method call with arrow ->
|
1010
|
+
elsif current_token[:type] == :operator && current_token[:value] == '->'
|
1011
|
+
arrow_token = advance # Skip '->'
|
1012
|
+
|
1013
|
+
# Next token should be an identifier (method name)
|
1014
|
+
if !current_token || current_token[:type] != :identifier
|
1015
|
+
@errors << {
|
1016
|
+
message: "Expected method name after '->'",
|
1017
|
+
position: @position,
|
1018
|
+
line: current_token ? current_token[:line] : 0,
|
1019
|
+
column: current_token ? current_token[:column] : 0
|
1020
|
+
}
|
1021
|
+
break
|
1022
|
+
end
|
1023
|
+
|
1024
|
+
method_name = advance[:value] # Get method name
|
1025
|
+
|
1026
|
+
# Check for arguments
|
1027
|
+
args = []
|
1028
|
+
if current_token && current_token[:type] == :paren_open
|
1029
|
+
expect(:paren_open) # Skip '('
|
1030
|
+
|
1031
|
+
# Parse arguments if any
|
1032
|
+
unless current_token && current_token[:type] == :paren_close
|
1033
|
+
loop do
|
1034
|
+
arg = parse_expression
|
1035
|
+
args << arg if arg
|
1036
|
+
|
1037
|
+
if current_token && current_token[:type] == :comma
|
1038
|
+
advance # Skip comma
|
1039
|
+
else
|
1040
|
+
break
|
1041
|
+
end
|
1042
|
+
end
|
1043
|
+
end
|
1044
|
+
|
1045
|
+
expect(:paren_close) # Skip ')'
|
1046
|
+
end
|
1047
|
+
|
1048
|
+
expr = {
|
1049
|
+
type: :method_call,
|
1050
|
+
object: expr,
|
1051
|
+
method: method_name,
|
1052
|
+
arguments: args,
|
1053
|
+
line: arrow_token[:line],
|
1054
|
+
column: arrow_token[:column]
|
1055
|
+
}
|
1056
|
+
# Check for indexing with brackets
|
1057
|
+
elsif current_token[:type] == :bracket_open
|
1058
|
+
bracket_token = advance # Skip '['
|
1059
|
+
|
1060
|
+
index_expr = parse_expression
|
1061
|
+
|
1062
|
+
expect(:bracket_close) # Skip ']'
|
1063
|
+
|
1064
|
+
expr = {
|
1065
|
+
type: :indexed_access,
|
1066
|
+
object: expr,
|
1067
|
+
index: index_expr,
|
1068
|
+
line: bracket_token[:line],
|
1069
|
+
column: bracket_token[:column]
|
1070
|
+
}
|
1071
|
+
# Check for function call directly on an expression
|
1072
|
+
elsif current_token[:type] == :paren_open
|
1073
|
+
paren_token = advance # Skip '('
|
1074
|
+
|
1075
|
+
args = []
|
1076
|
+
|
1077
|
+
# Parse arguments if any
|
1078
|
+
unless current_token && current_token[:type] == :paren_close
|
1079
|
+
loop do
|
1080
|
+
arg = parse_expression
|
1081
|
+
args << arg if arg
|
1082
|
+
|
1083
|
+
if current_token && current_token[:type] == :comma
|
1084
|
+
advance # Skip comma
|
1085
|
+
else
|
1086
|
+
break
|
1087
|
+
end
|
1088
|
+
end
|
1089
|
+
end
|
1090
|
+
|
1091
|
+
expect(:paren_close) # Skip ')'
|
1092
|
+
|
1093
|
+
expr = {
|
1094
|
+
type: :call_expression,
|
1095
|
+
callee: expr,
|
1096
|
+
arguments: args,
|
1097
|
+
line: paren_token[:line],
|
1098
|
+
column: paren_token[:column]
|
1099
|
+
}
|
1100
|
+
else
|
1101
|
+
# No more chaining
|
1102
|
+
break
|
1103
|
+
end
|
1104
|
+
end
|
1105
|
+
|
1106
|
+
return expr
|
1107
|
+
end
|
1108
|
+
|
1109
|
+
def is_lambda_expression
|
1110
|
+
# Save the current position
|
1111
|
+
original_position = @position
|
1112
|
+
|
1113
|
+
# Skip the opening parenthesis
|
1114
|
+
advance if current_token && current_token[:type] == :paren_open
|
1115
|
+
|
1116
|
+
# Check for a parameter list followed by => or ): =>
|
1117
|
+
has_params = false
|
1118
|
+
has_arrow = false
|
1119
|
+
|
1120
|
+
# Skip past parameters
|
1121
|
+
loop do
|
1122
|
+
break if !current_token
|
1123
|
+
|
1124
|
+
if current_token[:type] == :identifier
|
1125
|
+
has_params = true
|
1126
|
+
advance
|
1127
|
+
|
1128
|
+
# Either comma for another parameter, or close paren
|
1129
|
+
if current_token && current_token[:type] == :comma
|
1130
|
+
advance
|
1131
|
+
elsif current_token && current_token[:type] == :paren_close
|
1132
|
+
advance
|
1133
|
+
break
|
1134
|
+
else
|
1135
|
+
# Not a valid lambda parameter list
|
1136
|
+
break
|
1137
|
+
end
|
1138
|
+
elsif current_token[:type] == :paren_close
|
1139
|
+
# Empty parameter list is valid
|
1140
|
+
advance
|
1141
|
+
break
|
1142
|
+
else
|
1143
|
+
# Not a valid lambda parameter list
|
1144
|
+
break
|
1145
|
+
end
|
1146
|
+
end
|
1147
|
+
|
1148
|
+
# After parameters, check for either => or ): =>
|
1149
|
+
if current_token
|
1150
|
+
if current_token[:type] == :operator && current_token[:value] == '=>'
|
1151
|
+
has_arrow = true
|
1152
|
+
elsif current_token[:type] == :colon
|
1153
|
+
# There might be a return type annotation
|
1154
|
+
advance
|
1155
|
+
# Skip the type
|
1156
|
+
advance if current_token && current_token[:type] == :identifier
|
1157
|
+
# Check for the arrow
|
1158
|
+
has_arrow = current_token && current_token[:type] == :operator && current_token[:value] == '=>'
|
1159
|
+
end
|
1160
|
+
end
|
1161
|
+
|
1162
|
+
# Reset position to where we started
|
1163
|
+
@position = original_position
|
1164
|
+
|
1165
|
+
return has_params && has_arrow
|
1166
|
+
end
|
1167
|
+
|
1168
|
+
def parse_lambda_expression(line, column)
|
1169
|
+
expect(:paren_open) # Skip '('
|
1170
|
+
|
1171
|
+
params = []
|
1172
|
+
|
1173
|
+
# Parse parameters
|
1174
|
+
unless current_token && current_token[:type] == :paren_close
|
1175
|
+
loop do
|
1176
|
+
if current_token && current_token[:type] == :identifier
|
1177
|
+
param_name = current_token[:value]
|
1178
|
+
param_token = current_token
|
1179
|
+
advance
|
1180
|
+
|
1181
|
+
# Check for type annotation
|
1182
|
+
param_type = nil
|
1183
|
+
if current_token && current_token[:type] == :colon
|
1184
|
+
advance # Skip ':'
|
1185
|
+
param_type = parse_type
|
1186
|
+
end
|
1187
|
+
|
1188
|
+
params << {
|
1189
|
+
type: :parameter,
|
1190
|
+
name: param_name,
|
1191
|
+
param_type: param_type,
|
1192
|
+
line: param_token[:line],
|
1193
|
+
column: param_token[:column]
|
1194
|
+
}
|
1195
|
+
else
|
1196
|
+
@errors << {
|
1197
|
+
message: "Expected parameter name in lambda expression",
|
1198
|
+
position: @position,
|
1199
|
+
line: current_token ? current_token[:line] : 0,
|
1200
|
+
column: current_token ? current_token[:column] : 0
|
1201
|
+
}
|
1202
|
+
break
|
1203
|
+
end
|
1204
|
+
|
1205
|
+
if current_token && current_token[:type] == :comma
|
1206
|
+
advance # Skip comma
|
1207
|
+
else
|
1208
|
+
break
|
1209
|
+
end
|
1210
|
+
end
|
1211
|
+
end
|
1212
|
+
|
1213
|
+
expect(:paren_close) # Skip ')'
|
1214
|
+
|
1215
|
+
# Check for return type annotation
|
1216
|
+
return_type = nil
|
1217
|
+
if current_token && current_token[:type] == :colon
|
1218
|
+
advance # Skip ':'
|
1219
|
+
return_type = parse_type
|
1220
|
+
end
|
1221
|
+
|
1222
|
+
# Expect the arrow '=>'
|
1223
|
+
arrow_found = false
|
1224
|
+
if current_token && current_token[:type] == :operator && current_token[:value] == '=>'
|
1225
|
+
arrow_found = true
|
1226
|
+
advance # Skip '=>'
|
1227
|
+
else
|
1228
|
+
@errors << {
|
1229
|
+
message: "Expected '=>' in lambda expression",
|
1230
|
+
position: @position,
|
1231
|
+
line: current_token ? current_token[:line] : 0,
|
1232
|
+
column: current_token ? current_token[:column] : 0
|
1233
|
+
}
|
1234
|
+
end
|
1235
|
+
|
1236
|
+
# Parse the lambda body - check if it's a block or an expression
|
1237
|
+
if arrow_found
|
1238
|
+
if current_token && current_token[:type] == :brace_open
|
1239
|
+
# Parse block body
|
1240
|
+
advance # Skip '{'
|
1241
|
+
|
1242
|
+
block_body = []
|
1243
|
+
brace_count = 1 # Track nested braces
|
1244
|
+
|
1245
|
+
while @position < @tokens.length && brace_count > 0
|
1246
|
+
if current_token[:type] == :brace_open
|
1247
|
+
brace_count += 1
|
1248
|
+
elsif current_token[:type] == :brace_close
|
1249
|
+
brace_count -= 1
|
1250
|
+
|
1251
|
+
# Skip the final closing brace, but don't process it as a statement
|
1252
|
+
if brace_count == 0
|
1253
|
+
advance
|
1254
|
+
break
|
1255
|
+
end
|
1256
|
+
end
|
1257
|
+
|
1258
|
+
stmt = parse_statement
|
1259
|
+
block_body << stmt if stmt
|
1260
|
+
|
1261
|
+
# Break if we've reached the end or found the matching closing brace
|
1262
|
+
if brace_count == 0 || @position >= @tokens.length
|
1263
|
+
break
|
1264
|
+
end
|
1265
|
+
end
|
1266
|
+
|
1267
|
+
body = {
|
1268
|
+
type: :block_expression,
|
1269
|
+
statements: block_body,
|
1270
|
+
line: line,
|
1271
|
+
column: column
|
1272
|
+
}
|
1273
|
+
else
|
1274
|
+
# Simple expression body
|
1275
|
+
body = parse_expression
|
1276
|
+
end
|
1277
|
+
else
|
1278
|
+
body = nil
|
1279
|
+
end
|
1280
|
+
|
1281
|
+
return {
|
1282
|
+
type: :lambda_expression,
|
1283
|
+
params: params,
|
1284
|
+
return_type: return_type,
|
1285
|
+
body: body,
|
1286
|
+
line: line,
|
1287
|
+
column: column
|
1288
|
+
}
|
1289
|
+
end
|
1290
|
+
|
1291
|
+
def parse_dict_literal(line, column)
|
1292
|
+
advance # Skip '{'
|
1293
|
+
entries = []
|
1294
|
+
|
1295
|
+
# Empty dictionary
|
1296
|
+
if current_token && current_token[:type] == :brace_close
|
1297
|
+
advance # Skip '}'
|
1298
|
+
return {
|
1299
|
+
type: :dict_literal,
|
1300
|
+
entries: entries,
|
1301
|
+
line: line,
|
1302
|
+
column: column
|
1303
|
+
}
|
1304
|
+
end
|
1305
|
+
|
1306
|
+
# Parse dictionary entries
|
1307
|
+
loop do
|
1308
|
+
# Parse key (string or identifier)
|
1309
|
+
key = nil
|
1310
|
+
if current_token && (current_token[:type] == :string || current_token[:type] == :identifier)
|
1311
|
+
key = current_token[:type] == :string ? current_token[:value] : current_token[:value]
|
1312
|
+
advance # Skip key
|
1313
|
+
else
|
1314
|
+
@errors << {
|
1315
|
+
message: "Expected string or identifier as dictionary key",
|
1316
|
+
position: @position,
|
1317
|
+
line: current_token ? current_token[:line] : 0,
|
1318
|
+
column: current_token ? current_token[:column] : 0
|
1319
|
+
}
|
1320
|
+
break
|
1321
|
+
end
|
1322
|
+
|
1323
|
+
# Expect colon
|
1324
|
+
expect(:colon)
|
1325
|
+
|
1326
|
+
# Parse value
|
1327
|
+
value = parse_expression
|
1328
|
+
|
1329
|
+
entries << {
|
1330
|
+
key: key,
|
1331
|
+
value: value
|
1332
|
+
}
|
1333
|
+
|
1334
|
+
if current_token && current_token[:type] == :comma
|
1335
|
+
advance # Skip comma
|
1336
|
+
# Allow trailing comma
|
1337
|
+
break if current_token && current_token[:type] == :brace_close
|
1338
|
+
else
|
1339
|
+
break
|
1340
|
+
end
|
1341
|
+
end
|
1342
|
+
|
1343
|
+
expect(:brace_close) # Expect and skip '}'
|
1344
|
+
|
1345
|
+
{
|
1346
|
+
type: :dict_literal,
|
1347
|
+
entries: entries,
|
1348
|
+
line: line,
|
1349
|
+
column: column
|
1350
|
+
}
|
1351
|
+
end
|
1352
|
+
|
1353
|
+
def parse_list_literal(line, column)
|
1354
|
+
advance # Skip '['
|
1355
|
+
elements = []
|
1356
|
+
|
1357
|
+
# Empty list
|
1358
|
+
if current_token && current_token[:type] == :bracket_close
|
1359
|
+
advance # Skip ']'
|
1360
|
+
return {
|
1361
|
+
type: :list_literal,
|
1362
|
+
elements: elements,
|
1363
|
+
line: line,
|
1364
|
+
column: column
|
1365
|
+
}
|
1366
|
+
end
|
1367
|
+
|
1368
|
+
# Parse list elements
|
1369
|
+
while @position < @tokens.length
|
1370
|
+
# Check if we've reached the end of the list
|
1371
|
+
if current_token && current_token[:type] == :bracket_close
|
1372
|
+
advance # Skip ']'
|
1373
|
+
break
|
1374
|
+
end
|
1375
|
+
|
1376
|
+
element = parse_expression
|
1377
|
+
elements << element if element
|
1378
|
+
|
1379
|
+
# Continue if there's a comma
|
1380
|
+
if current_token && current_token[:type] == :comma
|
1381
|
+
advance # Skip comma
|
1382
|
+
else
|
1383
|
+
# If no comma and not a closing bracket, then it's an error
|
1384
|
+
if current_token && current_token[:type] != :bracket_close
|
1385
|
+
@errors << {
|
1386
|
+
message: "Expected comma or closing bracket after list element",
|
1387
|
+
position: @position,
|
1388
|
+
line: current_token[:line],
|
1389
|
+
column: current_token[:column]
|
1390
|
+
}
|
1391
|
+
end
|
1392
|
+
|
1393
|
+
# We still want to skip the closing bracket if it's there
|
1394
|
+
if current_token && current_token[:type] == :bracket_close
|
1395
|
+
advance
|
1396
|
+
end
|
1397
|
+
|
1398
|
+
break
|
1399
|
+
end
|
1400
|
+
end
|
1401
|
+
|
1402
|
+
# Check if we have a second list immediately following this one (Vim's special syntax)
|
1403
|
+
if current_token && current_token[:type] == :bracket_open
|
1404
|
+
next_list = parse_list_literal(current_token[:line], current_token[:column])
|
1405
|
+
|
1406
|
+
return {
|
1407
|
+
type: :list_concat_expression,
|
1408
|
+
left: {
|
1409
|
+
type: :list_literal,
|
1410
|
+
elements: elements,
|
1411
|
+
line: line,
|
1412
|
+
column: column
|
1413
|
+
},
|
1414
|
+
right: next_list,
|
1415
|
+
line: line,
|
1416
|
+
column: column
|
1417
|
+
}
|
1418
|
+
end
|
1419
|
+
|
1420
|
+
return {
|
1421
|
+
type: :list_literal,
|
1422
|
+
elements: elements,
|
1423
|
+
line: line,
|
1424
|
+
column: column
|
1425
|
+
}
|
1426
|
+
end
|
1427
|
+
|
1428
|
+
def parse_unary_expression
|
1429
|
+
# Check for unary operators (!, -, +)
|
1430
|
+
if current_token && current_token[:type] == :operator &&
|
1431
|
+
['!', '-', '+'].include?(current_token[:value])
|
1432
|
+
|
1433
|
+
op_token = advance # Skip the operator
|
1434
|
+
operand = parse_unary_expression # Recursively parse the operand
|
1435
|
+
|
1436
|
+
return {
|
1437
|
+
type: :unary_expression,
|
1438
|
+
operator: op_token[:value],
|
1439
|
+
operand: operand,
|
1440
|
+
line: op_token[:line],
|
1441
|
+
column: op_token[:column]
|
1442
|
+
}
|
1443
|
+
end
|
1444
|
+
|
1445
|
+
# If no unary operator, parse a primary expression
|
1446
|
+
return parse_primary_expression
|
662
1447
|
end
|
663
1448
|
|
664
1449
|
def parse_function_call(name, line, column)
|
@@ -666,14 +1451,40 @@ module Vinter
|
|
666
1451
|
|
667
1452
|
args = []
|
668
1453
|
|
669
|
-
# Parse arguments
|
670
|
-
|
671
|
-
|
1454
|
+
# Parse arguments until we find a closing parenthesis
|
1455
|
+
while @position < @tokens.length && current_token && current_token[:type] != :paren_close
|
1456
|
+
# Skip comments inside parameter lists
|
1457
|
+
if current_token && current_token[:type] == :comment
|
1458
|
+
advance
|
1459
|
+
next
|
1460
|
+
end
|
1461
|
+
|
1462
|
+
# Check if the argument might be a lambda expression
|
1463
|
+
if current_token && current_token[:type] == :paren_open && is_lambda_expression
|
1464
|
+
arg = parse_lambda_expression(current_token[:line], current_token[:column])
|
1465
|
+
else
|
672
1466
|
arg = parse_expression
|
673
|
-
|
1467
|
+
end
|
674
1468
|
|
675
|
-
|
676
|
-
|
1469
|
+
args << arg if arg
|
1470
|
+
|
1471
|
+
# Break if we hit the closing paren
|
1472
|
+
if current_token && current_token[:type] == :paren_close
|
1473
|
+
break
|
1474
|
+
end
|
1475
|
+
|
1476
|
+
# If we have a comma, advance past it and continue
|
1477
|
+
if current_token && current_token[:type] == :comma
|
1478
|
+
advance
|
1479
|
+
# If we don't have a comma and we're not at the end, it's an error
|
1480
|
+
elsif current_token && current_token[:type] != :paren_close && current_token[:type] != :comment
|
1481
|
+
@errors << {
|
1482
|
+
message: "Expected comma or closing parenthesis after argument",
|
1483
|
+
position: @position,
|
1484
|
+
line: current_token[:line],
|
1485
|
+
column: current_token[:column]
|
1486
|
+
}
|
1487
|
+
break
|
677
1488
|
end
|
678
1489
|
end
|
679
1490
|
|
@@ -837,7 +1648,37 @@ module Vinter
|
|
837
1648
|
advance # Skip '!'
|
838
1649
|
end
|
839
1650
|
|
840
|
-
|
1651
|
+
# For script-local functions or other scoped functions
|
1652
|
+
is_script_local = false
|
1653
|
+
function_scope = nil
|
1654
|
+
|
1655
|
+
# Check if we have a script-local function (s:)
|
1656
|
+
if current_token && current_token[:type] == :script_local
|
1657
|
+
is_script_local = true
|
1658
|
+
function_scope = current_token[:value]
|
1659
|
+
advance # Skip s: prefix
|
1660
|
+
elsif current_token && current_token[:type] == :identifier && current_token[:value].include?(':')
|
1661
|
+
# Handle other scoped functions like g: or b:
|
1662
|
+
parts = current_token[:value].split(':')
|
1663
|
+
if parts.length == 2
|
1664
|
+
function_scope = parts[0] + ':'
|
1665
|
+
advance
|
1666
|
+
end
|
1667
|
+
end
|
1668
|
+
|
1669
|
+
# Now handle the function name, which might be separate from the scope
|
1670
|
+
name = nil
|
1671
|
+
if !is_script_local && function_scope.nil? && current_token && current_token[:type] == :identifier
|
1672
|
+
name = current_token
|
1673
|
+
advance
|
1674
|
+
elsif is_script_local || !function_scope.nil?
|
1675
|
+
if current_token && current_token[:type] == :identifier
|
1676
|
+
name = current_token
|
1677
|
+
advance
|
1678
|
+
end
|
1679
|
+
else
|
1680
|
+
name = expect(:identifier)
|
1681
|
+
end
|
841
1682
|
|
842
1683
|
# Parse parameter list
|
843
1684
|
expect(:paren_open)
|
@@ -846,8 +1687,11 @@ module Vinter
|
|
846
1687
|
|
847
1688
|
# Check for optional attributes (range, dict, abort, closure)
|
848
1689
|
attributes = []
|
849
|
-
while current_token
|
850
|
-
if [
|
1690
|
+
while current_token
|
1691
|
+
if current_token[:type] == :keyword && current_token[:value] == 'abort'
|
1692
|
+
attributes << advance[:value]
|
1693
|
+
elsif current_token[:type] == :identifier &&
|
1694
|
+
['range', 'dict', 'closure'].include?(current_token[:value])
|
851
1695
|
attributes << advance[:value]
|
852
1696
|
else
|
853
1697
|
break
|
@@ -878,9 +1722,16 @@ module Vinter
|
|
878
1722
|
}
|
879
1723
|
end
|
880
1724
|
|
1725
|
+
function_name = name ? name[:value] : nil
|
1726
|
+
if function_scope
|
1727
|
+
function_name = function_scope + function_name if function_name
|
1728
|
+
end
|
1729
|
+
|
881
1730
|
{
|
882
1731
|
type: :legacy_function,
|
883
|
-
name:
|
1732
|
+
name: function_name,
|
1733
|
+
is_script_local: is_script_local,
|
1734
|
+
scope: function_scope,
|
884
1735
|
params: params,
|
885
1736
|
has_bang: has_bang,
|
886
1737
|
attributes: attributes,
|
@@ -890,7 +1741,7 @@ module Vinter
|
|
890
1741
|
}
|
891
1742
|
end
|
892
1743
|
|
893
|
-
# Legacy function parameters are different - they use a:name syntax
|
1744
|
+
# Legacy function parameters are different - they can use a:name syntax
|
894
1745
|
def parse_parameter_list_legacy
|
895
1746
|
params = []
|
896
1747
|
|
@@ -907,7 +1758,28 @@ module Vinter
|
|
907
1758
|
break
|
908
1759
|
end
|
909
1760
|
|
910
|
-
if
|
1761
|
+
if current_token && current_token[:type] == :identifier
|
1762
|
+
# Regular parameter
|
1763
|
+
param_name = advance
|
1764
|
+
params << {
|
1765
|
+
type: :parameter,
|
1766
|
+
name: param_name[:value],
|
1767
|
+
line: param_name[:line],
|
1768
|
+
column: param_name[:column]
|
1769
|
+
}
|
1770
|
+
elsif current_token && current_token[:type] == :arg_variable
|
1771
|
+
# Parameter with a: prefix
|
1772
|
+
param_name = advance
|
1773
|
+
# Extract name without 'a:' prefix
|
1774
|
+
name_without_prefix = param_name[:value].sub(/^a:/, '')
|
1775
|
+
params << {
|
1776
|
+
type: :parameter,
|
1777
|
+
name: name_without_prefix,
|
1778
|
+
line: param_name[:line],
|
1779
|
+
column: param_name[:column],
|
1780
|
+
is_arg_prefixed: true
|
1781
|
+
}
|
1782
|
+
else
|
911
1783
|
@errors << {
|
912
1784
|
message: "Expected parameter name",
|
913
1785
|
position: @position,
|
@@ -917,15 +1789,6 @@ module Vinter
|
|
917
1789
|
break
|
918
1790
|
end
|
919
1791
|
|
920
|
-
param_name = advance
|
921
|
-
|
922
|
-
params << {
|
923
|
-
type: :parameter,
|
924
|
-
name: param_name[:value],
|
925
|
-
line: param_name[:line],
|
926
|
-
column: param_name[:column]
|
927
|
-
}
|
928
|
-
|
929
1792
|
if current_token && current_token[:type] == :comma
|
930
1793
|
advance
|
931
1794
|
else
|