vinter 0.2.0 → 0.4.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 +12 -12
- data/bin/vinter +0 -0
- data/lib/vinter/cli.rb +49 -21
- data/lib/vinter/lexer.rb +468 -36
- data/lib/vinter/linter.rb +28 -3
- data/lib/vinter/parser.rb +3808 -577
- data/lib/vinter.rb +1 -1
- metadata +5 -4
data/lib/vinter/parser.rb
CHANGED
@@ -1,10 +1,11 @@
|
|
1
1
|
module Vinter
|
2
2
|
class Parser
|
3
|
-
def initialize(tokens)
|
3
|
+
def initialize(tokens, source_text = nil)
|
4
4
|
@tokens = tokens
|
5
5
|
@position = 0
|
6
6
|
@errors = []
|
7
7
|
@warnings = []
|
8
|
+
@source_text = source_text
|
8
9
|
end
|
9
10
|
|
10
11
|
def parse
|
@@ -42,8 +43,30 @@ module Vinter
|
|
42
43
|
found = current_token ? current_token[:type] : "end of input"
|
43
44
|
line = current_token ? current_token[:line] : 0
|
44
45
|
column = current_token ? current_token[:column] : 0
|
46
|
+
|
47
|
+
# Get the full line content from the input if available
|
48
|
+
line_content = nil
|
49
|
+
if line > 0 && @tokens.length > 0
|
50
|
+
# Find tokens on the same line
|
51
|
+
same_line_tokens = @tokens.select { |t| t[:line] == line }
|
52
|
+
if !same_line_tokens.empty?
|
53
|
+
# Create a representation of the line with a marker at the error position
|
54
|
+
line_representation = same_line_tokens.map { |t| t[:value] }.join('')
|
55
|
+
line_content = "Line content: #{line_representation}"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
45
59
|
error = "Expected #{expected} but found #{found}"
|
46
|
-
|
60
|
+
error += ", at line #{line}, column #{column}"
|
61
|
+
error += "\n#{line_content}" if line_content
|
62
|
+
|
63
|
+
@errors << {
|
64
|
+
message: error,
|
65
|
+
position: @position,
|
66
|
+
line: line,
|
67
|
+
column: column,
|
68
|
+
line_content: line_content
|
69
|
+
}
|
47
70
|
nil
|
48
71
|
end
|
49
72
|
end
|
@@ -65,15 +88,153 @@ module Vinter
|
|
65
88
|
{ type: :program, body: statements }
|
66
89
|
end
|
67
90
|
|
91
|
+
def parse_mapping_statement
|
92
|
+
token = current_token
|
93
|
+
advance # Skip the mapping command
|
94
|
+
line = token[:line]
|
95
|
+
column = token[:column]
|
96
|
+
mapping_command = token[:value]
|
97
|
+
|
98
|
+
# Parse options (like <silent>, <buffer>, etc.)
|
99
|
+
options = []
|
100
|
+
while current_token &&
|
101
|
+
(current_token[:type] == :identifier || current_token[:type] == :special_key) &&
|
102
|
+
current_token[:value].start_with?('<') &&
|
103
|
+
current_token[:value].end_with?('>')
|
104
|
+
options << current_token[:value]
|
105
|
+
advance
|
106
|
+
end
|
107
|
+
|
108
|
+
# Parse the key sequence
|
109
|
+
key_sequence = nil
|
110
|
+
if current_token
|
111
|
+
if current_token[:type] == :special_key ||
|
112
|
+
(current_token[:value].start_with?('<') && current_token[:value].end_with?('>'))
|
113
|
+
key_sequence = current_token[:value]
|
114
|
+
advance
|
115
|
+
else
|
116
|
+
key_sequence = current_token[:value]
|
117
|
+
advance
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# Collect everything else until end of line as the mapping target
|
122
|
+
# This is raw text and shouldn't be parsed as an expression
|
123
|
+
target_tokens = []
|
124
|
+
|
125
|
+
# Continue collecting tokens until we hit a newline or comment
|
126
|
+
while current_token &&
|
127
|
+
current_token[:type] != :comment &&
|
128
|
+
(current_token[:type] != :whitespace || current_token[:value].strip != '')
|
129
|
+
|
130
|
+
# If we hit a newline not preceded by a continuation character, we're done
|
131
|
+
if current_token[:value] == "\n" &&
|
132
|
+
(target_tokens.empty? || target_tokens.last[:value][-1] != '\\')
|
133
|
+
break
|
134
|
+
end
|
135
|
+
|
136
|
+
target_tokens << current_token
|
137
|
+
advance
|
138
|
+
end
|
139
|
+
|
140
|
+
# Join the target tokens to form the raw mapping target
|
141
|
+
target = target_tokens.map { |t| t[:value] }.join('')
|
142
|
+
|
143
|
+
{
|
144
|
+
type: :mapping_statement,
|
145
|
+
command: mapping_command,
|
146
|
+
options: options,
|
147
|
+
key_sequence: key_sequence,
|
148
|
+
target: target,
|
149
|
+
line: line,
|
150
|
+
column: column
|
151
|
+
}
|
152
|
+
end
|
153
|
+
|
68
154
|
def parse_statement
|
69
155
|
if !current_token
|
70
156
|
return nil
|
71
157
|
end
|
158
|
+
start_token = current_token
|
72
159
|
|
73
|
-
|
160
|
+
# Handle pipe as command separator
|
161
|
+
if current_token[:type] == :operator && current_token[:value] == '|'
|
162
|
+
advance # Skip the pipe
|
163
|
+
return parse_statement
|
164
|
+
end
|
165
|
+
|
166
|
+
# Handle endif keyword outside normal if structure (likely from one-line if)
|
167
|
+
if current_token[:type] == :keyword && current_token[:value] == 'endif'
|
168
|
+
token = advance # Skip the endif
|
169
|
+
return {
|
170
|
+
type: :endif_marker,
|
171
|
+
line: token[:line],
|
172
|
+
column: token[:column]
|
173
|
+
}
|
174
|
+
end
|
175
|
+
|
176
|
+
# Add special cases for other ending keywords too to be thorough
|
177
|
+
if current_token[:type] == :keyword &&
|
178
|
+
['endwhile', 'endfor', 'endfunction', 'endfunc', 'enddef'].include?(current_token[:value])
|
179
|
+
token = advance # Skip the keyword
|
180
|
+
return {
|
181
|
+
type: :"#{token[:value]}_marker",
|
182
|
+
line: token[:line],
|
183
|
+
column: token[:column]
|
184
|
+
}
|
185
|
+
end
|
186
|
+
|
187
|
+
# Now, add specific handling for continue and break
|
188
|
+
if current_token[:type] == :keyword && current_token[:value] == 'continue'
|
189
|
+
token = advance # Skip 'continue'
|
190
|
+
return {
|
191
|
+
type: :continue_statement,
|
192
|
+
line: token[:line],
|
193
|
+
column: token[:column]
|
194
|
+
}
|
195
|
+
end
|
196
|
+
|
197
|
+
if current_token[:type] == :keyword && current_token[:value] == 'break'
|
198
|
+
token = advance # Skip 'break'
|
199
|
+
return {
|
200
|
+
type: :break_statement,
|
201
|
+
line: token[:line],
|
202
|
+
column: token[:column]
|
203
|
+
}
|
204
|
+
end
|
205
|
+
|
206
|
+
# Handle try-catch
|
207
|
+
if current_token[:type] == :keyword && current_token[:value] == 'try'
|
208
|
+
return parse_try_statement
|
209
|
+
end
|
210
|
+
|
211
|
+
# Handle throw
|
212
|
+
if current_token[:type] == :keyword && current_token[:value] == 'throw'
|
213
|
+
return parse_throw_statement
|
214
|
+
end
|
215
|
+
|
216
|
+
# Skip standalone catch, finally, endtry keywords outside of try blocks
|
217
|
+
if current_token[:type] == :keyword &&
|
218
|
+
['catch', 'finally', 'endtry'].include?(current_token[:value])
|
219
|
+
token = advance # Skip these keywords
|
220
|
+
return nil
|
221
|
+
end
|
222
|
+
|
223
|
+
# Add case for mapping commands
|
224
|
+
if current_token[:type] == :keyword &&
|
225
|
+
['nnoremap', 'nmap', 'inoremap', 'imap', 'vnoremap', 'vmap',
|
226
|
+
'xnoremap', 'xmap', 'cnoremap', 'cmap', 'noremap', 'map'].include?(current_token[:value])
|
227
|
+
parse_mapping_statement
|
228
|
+
elsif current_token[:type] == :runtime_command
|
229
|
+
parse_runtime_statement
|
230
|
+
elsif current_token[:type] == :keyword && current_token[:value] == 'runtime'
|
231
|
+
parse_runtime_statement
|
232
|
+
elsif current_token[:type] == :keyword
|
74
233
|
case current_token[:value]
|
75
234
|
when 'if'
|
76
235
|
parse_if_statement
|
236
|
+
when 'command'
|
237
|
+
parse_command_definition
|
77
238
|
when 'while'
|
78
239
|
parse_while_statement
|
79
240
|
when 'for'
|
@@ -81,7 +242,18 @@ module Vinter
|
|
81
242
|
when 'def'
|
82
243
|
parse_def_function
|
83
244
|
when 'function'
|
84
|
-
|
245
|
+
# Only parse as function declaration if it looks like one
|
246
|
+
# Function declarations are followed by ! or identifier (function name)
|
247
|
+
if peek_token && (
|
248
|
+
(peek_token[:type] == :operator && peek_token[:value] == '!') ||
|
249
|
+
peek_token[:type] == :identifier ||
|
250
|
+
peek_token[:type] == :script_local
|
251
|
+
)
|
252
|
+
parse_legacy_function
|
253
|
+
else
|
254
|
+
# Not a function declaration, parse as expression statement
|
255
|
+
parse_expression_statement
|
256
|
+
end
|
85
257
|
when 'return'
|
86
258
|
parse_return_statement
|
87
259
|
when 'var', 'const', 'final'
|
@@ -93,6 +265,34 @@ module Vinter
|
|
93
265
|
when 'vim9script'
|
94
266
|
token = advance # Skip 'vim9script'
|
95
267
|
{ type: :vim9script_declaration, line: token[:line], column: token[:column] }
|
268
|
+
when 'autocmd'
|
269
|
+
parse_autocmd_statement
|
270
|
+
when 'execute', 'exec'
|
271
|
+
parse_execute_statement
|
272
|
+
when 'let'
|
273
|
+
parse_let_statement
|
274
|
+
when 'unlet'
|
275
|
+
parse_unlet_statement
|
276
|
+
when 'echohl', 'echomsg', 'echoerr', 'echom'
|
277
|
+
parse_echo_statement
|
278
|
+
when 'augroup'
|
279
|
+
parse_augroup_statement
|
280
|
+
when 'silent'
|
281
|
+
parse_silent_command
|
282
|
+
when 'call'
|
283
|
+
parse_call_statement
|
284
|
+
when 'delete'
|
285
|
+
parse_delete_statement
|
286
|
+
when 'set', 'setlocal'
|
287
|
+
parse_set_command
|
288
|
+
when 'syntax'
|
289
|
+
parse_syntax_command
|
290
|
+
when 'highlight'
|
291
|
+
parse_highlight_command
|
292
|
+
when 'sleep'
|
293
|
+
parse_sleep_command
|
294
|
+
when 'source'
|
295
|
+
parse_source_command
|
96
296
|
else
|
97
297
|
@warnings << {
|
98
298
|
message: "Unexpected keyword: #{current_token[:value]}",
|
@@ -104,9 +304,35 @@ module Vinter
|
|
104
304
|
nil
|
105
305
|
end
|
106
306
|
elsif current_token[:type] == :identifier
|
107
|
-
|
307
|
+
if current_token[:value] == "echo"
|
308
|
+
parse_echo_statement
|
309
|
+
elsif current_token[:value] == "augroup"
|
310
|
+
parse_augroup_statement
|
311
|
+
elsif current_token[:value] == "au" || current_token[:value] == "autocmd"
|
312
|
+
parse_autocmd_statement
|
313
|
+
elsif current_token[:value] == "filter" || current_token[:value] == "filt"
|
314
|
+
parse_filter_command
|
315
|
+
elsif current_token[:value] == "command"
|
316
|
+
parse_command_definition
|
317
|
+
else
|
318
|
+
parse_expression_statement
|
319
|
+
end
|
108
320
|
elsif current_token[:type] == :comment
|
109
321
|
parse_comment
|
322
|
+
elsif current_token[:type] == :string && current_token[:value].start_with?('"')
|
323
|
+
parse_comment
|
324
|
+
# token = current_token
|
325
|
+
# line = token[:line]
|
326
|
+
# column = token[:column]
|
327
|
+
# value = token[:value]
|
328
|
+
# advance
|
329
|
+
# { type: :comment, value: value, line: line, column: column }
|
330
|
+
elsif current_token[:type] == :silent_bang
|
331
|
+
parse_silent_command
|
332
|
+
elsif current_token[:type] == :identifier && current_token[:value] == 'delete'
|
333
|
+
parse_delete_command
|
334
|
+
elsif current_token[:type] == :percentage
|
335
|
+
parse_range_command
|
110
336
|
else
|
111
337
|
@warnings << {
|
112
338
|
message: "Unexpected token type: #{current_token[:type]}",
|
@@ -119,822 +345,3827 @@ module Vinter
|
|
119
345
|
end
|
120
346
|
end
|
121
347
|
|
122
|
-
def
|
123
|
-
|
124
|
-
line = current_token[:line]
|
125
|
-
column = current_token[:column]
|
126
|
-
advance
|
127
|
-
{ type: :comment, value: comment, line: line, column: column }
|
128
|
-
end
|
129
|
-
|
130
|
-
def parse_if_statement
|
131
|
-
token = advance # Skip 'if'
|
348
|
+
def parse_range_command
|
349
|
+
token = advance # Skip '%'
|
132
350
|
line = token[:line]
|
133
351
|
column = token[:column]
|
134
|
-
condition = parse_expression
|
135
352
|
|
136
|
-
|
137
|
-
|
353
|
+
# Parse the command that follows the range
|
354
|
+
command = parse_statement
|
138
355
|
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
356
|
+
{
|
357
|
+
type: :range_command,
|
358
|
+
range: '%',
|
359
|
+
command: command,
|
360
|
+
line: line,
|
361
|
+
column: column
|
362
|
+
}
|
363
|
+
end
|
145
364
|
|
146
|
-
|
147
|
-
|
148
|
-
|
365
|
+
def parse_runtime_statement
|
366
|
+
token = advance # Skip 'runtime' or 'runtime!'
|
367
|
+
line = token[:line]
|
368
|
+
column = token[:column]
|
149
369
|
|
150
|
-
|
151
|
-
|
152
|
-
if current_token[:value] == 'else'
|
153
|
-
advance # Skip 'else'
|
370
|
+
is_bang = token[:type] == :runtime_command ||
|
371
|
+
(token[:value] == 'runtime' && current_token && current_token[:type] == :operator && current_token[:value] == '!')
|
154
372
|
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
end
|
373
|
+
# Skip the '!' if it's separate token
|
374
|
+
if token[:type] != :runtime_command && is_bang
|
375
|
+
advance # Skip '!'
|
376
|
+
end
|
160
377
|
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
end
|
378
|
+
# Collect the pattern argument
|
379
|
+
pattern_parts = []
|
380
|
+
while @position < @tokens.length &&
|
381
|
+
!(current_token[:type] == :keyword || current_token[:value] == "\n")
|
382
|
+
pattern_parts << current_token[:value]
|
383
|
+
advance
|
168
384
|
end
|
169
385
|
|
170
|
-
|
171
|
-
expect(:keyword) # This should be 'endif'
|
386
|
+
pattern = pattern_parts.join('').strip
|
172
387
|
|
173
388
|
{
|
174
|
-
type: :
|
175
|
-
|
176
|
-
|
177
|
-
else_branch: else_branch,
|
389
|
+
type: :runtime_statement,
|
390
|
+
bang: is_bang,
|
391
|
+
pattern: pattern,
|
178
392
|
line: line,
|
179
393
|
column: column
|
180
394
|
}
|
181
395
|
end
|
182
396
|
|
183
|
-
def
|
184
|
-
token = advance # Skip '
|
397
|
+
def parse_filter_command
|
398
|
+
token = advance # Skip 'execute'
|
185
399
|
line = token[:line]
|
186
400
|
column = token[:column]
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
401
|
+
# lets count the parens that are created as we parse through the filter command
|
402
|
+
expect(:paren_open)
|
403
|
+
open_parens = 1
|
404
|
+
while open_parens > 0
|
405
|
+
advance
|
406
|
+
if current_token[:type] == :paren_open
|
407
|
+
open_parens += 1
|
408
|
+
elsif current_token[:type] == :paren_close
|
409
|
+
open_parens -= 1
|
195
410
|
end
|
196
|
-
|
197
|
-
stmt = parse_statement
|
198
|
-
body << stmt if stmt
|
199
411
|
end
|
412
|
+
advance
|
200
413
|
|
201
|
-
#
|
202
|
-
|
414
|
+
# Parse arguments - typically string expressions with concatenation
|
415
|
+
# Just accept any tokens until we hit a statement terminator or another command
|
416
|
+
#expressions = []
|
417
|
+
#expr = parse_expression
|
418
|
+
#expr = parse_string
|
419
|
+
#expressions << expr if expr
|
203
420
|
|
421
|
+
# Return the execute statement
|
204
422
|
{
|
205
|
-
type: :
|
206
|
-
|
207
|
-
body: body,
|
423
|
+
type: :filter_command,
|
424
|
+
#expressions: expressions,
|
208
425
|
line: line,
|
209
426
|
column: column
|
210
427
|
}
|
211
428
|
end
|
212
429
|
|
213
|
-
def
|
214
|
-
token = advance # Skip '
|
430
|
+
def old_parse_filter_command
|
431
|
+
token = advance # Skip 'filter' or 'filt'
|
215
432
|
line = token[:line]
|
216
433
|
column = token[:column]
|
217
434
|
|
218
|
-
#
|
219
|
-
|
220
|
-
|
221
|
-
|
435
|
+
# Check for bang (!)
|
436
|
+
has_bang = false
|
437
|
+
if current_token && current_token[:type] == :operator && current_token[:value] == '!'
|
438
|
+
has_bang = true
|
439
|
+
advance # Skip '!'
|
440
|
+
end
|
222
441
|
|
223
|
-
|
442
|
+
# Parse the pattern
|
443
|
+
pattern = nil
|
444
|
+
pattern_delimiter = nil
|
224
445
|
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
@errors << {
|
230
|
-
message: "Expected identifier in for loop variables",
|
231
|
-
position: @position,
|
232
|
-
line: current_token ? current_token[:line] : 0,
|
233
|
-
column: current_token ? current_token[:column] : 0
|
234
|
-
}
|
235
|
-
break
|
236
|
-
end
|
446
|
+
if current_token && current_token[:type] == :operator && current_token[:value] == '/'
|
447
|
+
# Handle /pattern/ form
|
448
|
+
pattern_delimiter = '/'
|
449
|
+
advance # Skip opening delimiter
|
237
450
|
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
451
|
+
# Collect all tokens until closing delimiter
|
452
|
+
pattern_parts = []
|
453
|
+
while @position < @tokens.length &&
|
454
|
+
!(current_token[:type] == :operator && current_token[:value] == pattern_delimiter)
|
455
|
+
pattern_parts << current_token[:value]
|
456
|
+
advance
|
243
457
|
end
|
244
458
|
|
245
|
-
|
459
|
+
pattern = pattern_parts.join('')
|
246
460
|
|
247
|
-
|
461
|
+
# Skip closing delimiter
|
462
|
+
if current_token && current_token[:type] == :operator && current_token[:value] == pattern_delimiter
|
463
|
+
advance
|
464
|
+
else
|
248
465
|
@errors << {
|
249
|
-
message: "Expected
|
466
|
+
message: "Expected closing pattern delimiter: #{pattern_delimiter}",
|
250
467
|
position: @position,
|
251
468
|
line: current_token ? current_token[:line] : 0,
|
252
469
|
column: current_token ? current_token[:column] : 0
|
253
470
|
}
|
254
|
-
else
|
255
|
-
advance # Skip 'in'
|
256
471
|
end
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
body = []
|
472
|
+
else
|
473
|
+
# Handle direct pattern form (without delimiters)
|
474
|
+
# Parse until we see what appears to be the command
|
475
|
+
pattern_parts = []
|
262
476
|
while @position < @tokens.length
|
263
|
-
|
477
|
+
# Don't consume tokens that likely belong to the command part
|
478
|
+
if current_token[:type] == :keyword ||
|
479
|
+
(current_token[:type] == :identifier &&
|
480
|
+
['echo', 'let', 'execute', 'exec', 'autocmd', 'au', 'oldfiles', 'clist', 'command',
|
481
|
+
'files', 'highlight', 'jumps', 'list', 'llist', 'marks', 'registers', 'set'].include?(current_token[:value]))
|
264
482
|
break
|
265
483
|
end
|
266
484
|
|
267
|
-
|
268
|
-
|
269
|
-
end
|
270
|
-
|
271
|
-
# Expect endfor
|
272
|
-
expect(:keyword) # This should be 'endfor'
|
273
|
-
|
274
|
-
return {
|
275
|
-
type: :for_statement,
|
276
|
-
loop_vars: loop_vars,
|
277
|
-
iterable: iterable,
|
278
|
-
body: body,
|
279
|
-
line: line,
|
280
|
-
column: column
|
281
|
-
}
|
282
|
-
else
|
283
|
-
# Simple for var in list
|
284
|
-
if !current_token || current_token[:type] != :identifier
|
285
|
-
@errors << {
|
286
|
-
message: "Expected identifier as for loop variable",
|
287
|
-
position: @position,
|
288
|
-
line: current_token ? current_token[:line] : 0,
|
289
|
-
column: current_token ? current_token[:column] : 0
|
290
|
-
}
|
291
|
-
return nil
|
485
|
+
pattern_parts << current_token[:value]
|
486
|
+
advance
|
292
487
|
end
|
293
488
|
|
294
|
-
|
295
|
-
|
296
|
-
if !current_token || (current_token[:type] != :identifier || current_token[:value] != 'in')
|
297
|
-
@errors << {
|
298
|
-
message: "Expected 'in' after for loop variable",
|
299
|
-
position: @position,
|
300
|
-
line: current_token ? current_token[:line] : 0,
|
301
|
-
column: current_token ? current_token[:column] : 0
|
302
|
-
}
|
303
|
-
else
|
304
|
-
advance # Skip 'in'
|
305
|
-
end
|
489
|
+
pattern = pattern_parts.join('').strip
|
490
|
+
end
|
306
491
|
|
307
|
-
|
492
|
+
# Parse the command to be filtered
|
493
|
+
command = parse_statement
|
308
494
|
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
495
|
+
{
|
496
|
+
type: :filter_command,
|
497
|
+
pattern: pattern,
|
498
|
+
has_bang: has_bang,
|
499
|
+
command: command,
|
500
|
+
line: line,
|
501
|
+
column: column
|
502
|
+
}
|
503
|
+
end
|
315
504
|
|
316
|
-
|
317
|
-
|
318
|
-
|
505
|
+
def parse_augroup_statement
|
506
|
+
token = advance # Skip 'augroup'
|
507
|
+
line = token[:line]
|
508
|
+
column = token[:column]
|
319
509
|
|
320
|
-
|
321
|
-
|
510
|
+
# Get the augroup name
|
511
|
+
name = nil
|
512
|
+
if current_token && current_token[:type] == :identifier
|
513
|
+
name = current_token[:value]
|
514
|
+
advance
|
515
|
+
else
|
516
|
+
@errors << {
|
517
|
+
message: "Expected augroup name",
|
518
|
+
position: @position,
|
519
|
+
line: current_token ? current_token[:line] : 0,
|
520
|
+
column: current_token ? current_token[:column] : 0
|
521
|
+
}
|
522
|
+
end
|
322
523
|
|
524
|
+
# Check for augroup END
|
525
|
+
is_end_marker = false
|
526
|
+
if name && (name.upcase == "END" || name == "END")
|
527
|
+
is_end_marker = true
|
323
528
|
return {
|
324
|
-
type: :
|
325
|
-
loop_var: loop_var,
|
326
|
-
iterable: iterable,
|
327
|
-
body: body,
|
529
|
+
type: :augroup_end,
|
328
530
|
line: line,
|
329
531
|
column: column
|
330
532
|
}
|
331
533
|
end
|
332
|
-
end
|
333
|
-
|
334
|
-
def parse_def_function
|
335
|
-
token = advance # Skip 'def'
|
336
|
-
line = token[:line]
|
337
|
-
column = token[:column]
|
338
|
-
|
339
|
-
name = expect(:identifier)
|
340
|
-
|
341
|
-
# Parse parameter list
|
342
|
-
expect(:paren_open)
|
343
|
-
params = parse_parameter_list
|
344
|
-
expect(:paren_close)
|
345
|
-
|
346
|
-
# Parse optional return type
|
347
|
-
return_type = nil
|
348
|
-
if current_token && current_token[:type] == :colon
|
349
|
-
advance # Skip ':'
|
350
|
-
return_type = parse_type
|
351
|
-
end
|
352
534
|
|
353
|
-
# Parse
|
535
|
+
# Parse statements within the augroup until we find 'augroup END'
|
354
536
|
body = []
|
355
537
|
while @position < @tokens.length
|
356
|
-
|
357
|
-
|
538
|
+
# Check for 'augroup END'
|
539
|
+
if (current_token[:type] == :keyword && current_token[:value] == 'augroup') ||
|
540
|
+
(current_token[:type] == :identifier && current_token[:value] == 'augroup')
|
541
|
+
# Look ahead for END
|
542
|
+
if peek_token &&
|
543
|
+
((peek_token[:type] == :identifier &&
|
544
|
+
(peek_token[:value].upcase == 'END' || peek_token[:value] == 'END')) ||
|
545
|
+
(peek_token[:type] == :keyword && peek_token[:value].upcase == 'END'))
|
546
|
+
advance # Skip 'augroup'
|
547
|
+
advance # Skip 'END'
|
548
|
+
break
|
549
|
+
end
|
358
550
|
end
|
359
551
|
|
360
552
|
stmt = parse_statement
|
361
553
|
body << stmt if stmt
|
362
554
|
end
|
363
555
|
|
364
|
-
# Expect enddef
|
365
|
-
expect(:keyword) # This should be 'enddef'
|
366
|
-
|
367
556
|
{
|
368
|
-
type: :
|
369
|
-
name: name
|
370
|
-
params: params,
|
371
|
-
return_type: return_type,
|
557
|
+
type: :augroup_statement,
|
558
|
+
name: name,
|
372
559
|
body: body,
|
373
560
|
line: line,
|
374
561
|
column: column
|
375
562
|
}
|
376
563
|
end
|
377
564
|
|
565
|
+
def parse_execute_statement
|
566
|
+
token = advance # Skip 'execute'
|
567
|
+
line = token[:line]
|
568
|
+
column = token[:column]
|
378
569
|
|
570
|
+
# Parse arguments - typically string expressions with concatenation
|
571
|
+
# Just accept any tokens until we hit a statement terminator or another command
|
572
|
+
expressions = []
|
573
|
+
expr = parse_expression
|
574
|
+
#expr = parse_string
|
575
|
+
expressions << expr if expr
|
379
576
|
|
380
|
-
|
381
|
-
|
577
|
+
# Return the execute statement
|
578
|
+
{
|
579
|
+
type: :execute_statement,
|
580
|
+
expressions: expressions,
|
581
|
+
line: line,
|
582
|
+
column: column
|
583
|
+
}
|
584
|
+
end
|
382
585
|
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
586
|
+
def parse_let_statement
|
587
|
+
# binding.pry
|
588
|
+
token = advance # Skip 'let'
|
589
|
+
line = token[:line]
|
590
|
+
column = token[:column]
|
387
591
|
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
592
|
+
# Parse the target variable
|
593
|
+
target = nil
|
594
|
+
if current_token
|
595
|
+
case current_token[:type]
|
596
|
+
when :identifier, :global_variable, :script_local, :buffer_local, :window_local, :tab_local, :arg_variable, :option_variable, :special_variable, :local_variable
|
597
|
+
target = {
|
598
|
+
type: current_token[:type],
|
599
|
+
name: current_token[:value],
|
600
|
+
line: current_token[:line],
|
601
|
+
column: current_token[:column]
|
602
|
+
}
|
603
|
+
advance
|
392
604
|
|
393
|
-
#
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
605
|
+
# Check for property access with dot notation (e.g., person.job)
|
606
|
+
if current_token && current_token[:type] == :operator && current_token[:value] == '.'
|
607
|
+
dot_token = advance # Skip '.'
|
608
|
+
|
609
|
+
# Next token should be an identifier (property name)
|
610
|
+
if current_token && (current_token[:type] == :identifier || current_token[:type] == :keyword)
|
611
|
+
property_token = advance # Get the property name
|
612
|
+
|
613
|
+
# Update the target to be a property access
|
614
|
+
target = {
|
615
|
+
type: :property_access,
|
616
|
+
object: target,
|
617
|
+
property: property_token[:value],
|
618
|
+
line: dot_token[:line],
|
619
|
+
column: dot_token[:column]
|
620
|
+
}
|
621
|
+
else
|
622
|
+
@errors << {
|
623
|
+
message: "Expected property name after '.'",
|
624
|
+
position: @position,
|
625
|
+
line: current_token ? current_token[:line] : 0,
|
626
|
+
column: current_token ? current_token[:column] : 0
|
627
|
+
}
|
628
|
+
end
|
398
629
|
end
|
399
630
|
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
631
|
+
# Check for chained indexed access (dictionary key lookup)
|
632
|
+
while current_token && current_token[:type] == :bracket_open
|
633
|
+
bracket_token = advance # Skip '['
|
634
|
+
index_expr = parse_expression
|
635
|
+
expect(:bracket_close) # Skip ']'
|
636
|
+
|
637
|
+
# Update target to be an indexed access
|
638
|
+
target = {
|
639
|
+
type: :indexed_access,
|
640
|
+
object: target,
|
641
|
+
index: index_expr,
|
642
|
+
line: bracket_token[:line],
|
643
|
+
column: bracket_token[:column]
|
644
|
+
}
|
645
|
+
end
|
646
|
+
when :register_access
|
647
|
+
target = {
|
648
|
+
type: :register_access,
|
649
|
+
register: current_token[:value],
|
650
|
+
line: current_token[:line],
|
651
|
+
column: current_token[:column]
|
405
652
|
}
|
406
|
-
|
407
|
-
|
653
|
+
advance
|
654
|
+
when :bracket_open
|
655
|
+
# Handle array destructuring like: let [key, value] = split(header, ': ')
|
656
|
+
bracket_token = advance # Skip '['
|
657
|
+
|
658
|
+
# Parse the variable names inside the brackets
|
659
|
+
destructuring_targets = []
|
660
|
+
|
661
|
+
# Parse comma-separated list of variables
|
662
|
+
while current_token && current_token[:type] != :bracket_close
|
663
|
+
# Skip commas between variables
|
664
|
+
if current_token[:type] == :comma
|
665
|
+
advance
|
666
|
+
next
|
667
|
+
end
|
408
668
|
|
409
|
-
|
410
|
-
|
669
|
+
# Parse the variable name
|
670
|
+
if current_token && (
|
671
|
+
current_token[:type] == :identifier ||
|
672
|
+
current_token[:type] == :global_variable ||
|
673
|
+
current_token[:type] == :script_local ||
|
674
|
+
current_token[:type] == :arg_variable ||
|
675
|
+
current_token[:type] == :option_variable ||
|
676
|
+
current_token[:type] == :special_variable ||
|
677
|
+
current_token[:type] == :local_variable
|
678
|
+
)
|
679
|
+
destructuring_targets << {
|
680
|
+
type: current_token[:type],
|
681
|
+
name: current_token[:value],
|
682
|
+
line: current_token[:line],
|
683
|
+
column: current_token[:column]
|
684
|
+
}
|
685
|
+
advance
|
686
|
+
else
|
687
|
+
@errors << {
|
688
|
+
message: "Expected variable name in destructuring assignment",
|
689
|
+
position: @position,
|
690
|
+
line: current_token ? current_token[:line] : 0,
|
691
|
+
column: current_token ? current_token[:column] : 0
|
692
|
+
}
|
693
|
+
# Try to recover by advancing to next comma or closing bracket
|
694
|
+
while current_token && current_token[:type] != :comma && current_token[:type] != :bracket_close
|
695
|
+
advance
|
696
|
+
end
|
697
|
+
end
|
698
|
+
end
|
699
|
+
|
700
|
+
expect(:bracket_close) # Skip ']'
|
701
|
+
|
702
|
+
target = {
|
703
|
+
type: :destructuring_assignment,
|
704
|
+
targets: destructuring_targets,
|
705
|
+
line: bracket_token[:line],
|
706
|
+
column: bracket_token[:column]
|
707
|
+
}
|
708
|
+
else
|
411
709
|
@errors << {
|
412
|
-
message: "Expected
|
710
|
+
message: "Expected variable name after let",
|
413
711
|
position: @position,
|
414
712
|
line: current_token ? current_token[:line] : 0,
|
415
713
|
column: current_token ? current_token[:column] : 0
|
416
714
|
}
|
417
|
-
break
|
418
715
|
end
|
716
|
+
end
|
419
717
|
|
420
|
-
|
718
|
+
# Skip the '=' or other assignment operator
|
719
|
+
operator = nil
|
720
|
+
if current_token && (
|
721
|
+
(current_token[:type] == :operator && current_token[:value] == '=') ||
|
722
|
+
current_token[:type] == :compound_operator)
|
723
|
+
operator = current_token[:value]
|
724
|
+
advance
|
725
|
+
else
|
726
|
+
@errors << {
|
727
|
+
message: "Expected assignment operator after variable in let statement",
|
728
|
+
position: @position,
|
729
|
+
line: current_token ? current_token[:line] : 0,
|
730
|
+
column: current_token ? current_token[:column] : 0
|
731
|
+
}
|
732
|
+
end
|
421
733
|
|
422
|
-
|
423
|
-
|
424
|
-
if current_token && current_token[:type] == :colon
|
425
|
-
advance # Skip ':'
|
426
|
-
param_type = parse_type
|
427
|
-
end
|
734
|
+
# Parse the value expression
|
735
|
+
value = nil
|
428
736
|
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
advance # Skip '='
|
433
|
-
default_value = parse_expression
|
434
|
-
end
|
737
|
+
# Special handling for function() references in let statements
|
738
|
+
if current_token && current_token[:type] == :keyword && current_token[:value] == 'function'
|
739
|
+
advance # Skip 'function'
|
435
740
|
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
param_type: param_type,
|
440
|
-
optional: false, # Set this based on default value
|
441
|
-
default_value: default_value,
|
442
|
-
line: param_name[:line],
|
443
|
-
column: param_name[:column]
|
444
|
-
}
|
741
|
+
# Expect opening parenthesis
|
742
|
+
if current_token && current_token[:type] == :paren_open
|
743
|
+
paren_token = advance # Skip '('
|
445
744
|
|
446
|
-
|
447
|
-
|
745
|
+
# Parse the function name as a string
|
746
|
+
func_name = nil
|
747
|
+
if current_token && current_token[:type] == :string
|
748
|
+
func_name = current_token[:value]
|
749
|
+
advance
|
750
|
+
else
|
751
|
+
@errors << {
|
752
|
+
message: "Expected string with function name in function() call",
|
753
|
+
position: @position,
|
754
|
+
line: current_token ? current_token[:line] : 0,
|
755
|
+
column: current_token ? current_token[:column] : 0
|
756
|
+
}
|
757
|
+
end
|
758
|
+
|
759
|
+
# Expect closing parenthesis
|
760
|
+
expect(:paren_close) # Skip ')'
|
761
|
+
|
762
|
+
value = {
|
763
|
+
type: :function_reference,
|
764
|
+
function_name: func_name,
|
765
|
+
line: line,
|
766
|
+
column: column
|
767
|
+
}
|
448
768
|
else
|
449
|
-
|
769
|
+
# If not followed by parenthesis, parse as a normal expression
|
770
|
+
@position -= 1 # Go back to the 'function' keyword
|
771
|
+
value = parse_expression
|
450
772
|
end
|
773
|
+
else
|
774
|
+
# Normal expression parsing
|
775
|
+
value = parse_expression
|
451
776
|
end
|
452
|
-
|
453
|
-
|
777
|
+
{
|
778
|
+
type: :let_statement,
|
779
|
+
target: target,
|
780
|
+
operator: operator,
|
781
|
+
value: value,
|
782
|
+
line: line,
|
783
|
+
column: column
|
784
|
+
}
|
454
785
|
end
|
455
786
|
|
456
|
-
def
|
457
|
-
|
458
|
-
|
787
|
+
def parse_unlet_statement
|
788
|
+
token = advance # Skip 'unlet'
|
789
|
+
line = token[:line]
|
790
|
+
column = token[:column]
|
459
791
|
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
792
|
+
# Parse the variable to unlet
|
793
|
+
target = nil
|
794
|
+
if current_token
|
795
|
+
case current_token[:type]
|
796
|
+
when :identifier, :global_variable, :script_local, :buffer_local, :window_local, :tab_local, :arg_variable, :option_variable, :special_variable, :local_variable
|
797
|
+
target = {
|
798
|
+
type: current_token[:type],
|
799
|
+
name: current_token[:value],
|
800
|
+
line: current_token[:line],
|
801
|
+
column: current_token[:column]
|
802
|
+
}
|
803
|
+
advance
|
465
804
|
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
805
|
+
# Check for chained indexed access (like variable[key])
|
806
|
+
while current_token && current_token[:type] == :bracket_open
|
807
|
+
bracket_token = advance # Skip '['
|
808
|
+
index_expr = parse_expression
|
809
|
+
expect(:bracket_close) # Skip ']'
|
810
|
+
|
811
|
+
# Update target to be an indexed access
|
812
|
+
target = {
|
813
|
+
type: :indexed_access,
|
814
|
+
object: target,
|
815
|
+
index: index_expr,
|
816
|
+
line: bracket_token[:line],
|
817
|
+
column: bracket_token[:column]
|
818
|
+
}
|
819
|
+
end
|
820
|
+
else
|
821
|
+
@errors << {
|
822
|
+
message: "Expected variable name after 'unlet'",
|
823
|
+
position: @position,
|
824
|
+
line: current_token ? current_token[:line] : 0,
|
825
|
+
column: current_token ? current_token[:column] : 0
|
472
826
|
}
|
473
827
|
end
|
474
|
-
|
475
|
-
return type_name[:value]
|
476
|
-
else
|
477
|
-
@errors << {
|
478
|
-
message: "Expected type identifier",
|
479
|
-
position: @position,
|
480
|
-
line: current_token ? current_token[:line] : 0,
|
481
|
-
column: current_token ? current_token[:column] : 0
|
482
|
-
}
|
483
|
-
advance
|
484
|
-
return "unknown"
|
485
828
|
end
|
829
|
+
|
830
|
+
{
|
831
|
+
type: :unlet_statement,
|
832
|
+
target: target,
|
833
|
+
line: line,
|
834
|
+
column: column
|
835
|
+
}
|
486
836
|
end
|
487
837
|
|
488
|
-
def
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
column = var_type_token[:column]
|
838
|
+
def parse_autocmd_statement
|
839
|
+
token = advance # Skip 'autocmd'
|
840
|
+
line = token[:line]
|
841
|
+
column = token[:column]
|
493
842
|
|
494
|
-
|
843
|
+
# Parse event name (like BufNewFile or VimEnter)
|
844
|
+
event = nil
|
845
|
+
if current_token && current_token[:type] == :identifier
|
846
|
+
event = advance[:value]
|
847
|
+
else
|
495
848
|
@errors << {
|
496
|
-
message: "Expected
|
849
|
+
message: "Expected event name after 'autocmd'",
|
497
850
|
position: @position,
|
498
851
|
line: current_token ? current_token[:line] : 0,
|
499
852
|
column: current_token ? current_token[:column] : 0
|
500
853
|
}
|
501
|
-
return nil
|
502
854
|
end
|
503
855
|
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
if current_token && current_token[:type] == :colon
|
510
|
-
advance # Skip ':'
|
511
|
-
var_type_annotation = parse_type
|
856
|
+
# Parse pattern (like *.match or *)
|
857
|
+
pattern = nil
|
858
|
+
if current_token
|
859
|
+
pattern = current_token[:value]
|
860
|
+
advance
|
512
861
|
end
|
513
862
|
|
514
|
-
# Parse
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
|
863
|
+
# Parse everything after as the command - collect as raw tokens
|
864
|
+
command_parts = []
|
865
|
+
while @position < @tokens.length
|
866
|
+
if !current_token || current_token[:type] == :comment ||
|
867
|
+
(current_token[:value] == "\n" && !current_token[:value].start_with?("\\"))
|
868
|
+
break
|
869
|
+
end
|
870
|
+
|
871
|
+
command_parts << current_token
|
872
|
+
advance
|
519
873
|
end
|
520
874
|
|
521
|
-
{
|
522
|
-
type: :
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
initializer: initializer,
|
875
|
+
return {
|
876
|
+
type: :autocmd_statement,
|
877
|
+
event: event,
|
878
|
+
pattern: pattern,
|
879
|
+
command_parts: command_parts,
|
527
880
|
line: line,
|
528
881
|
column: column
|
529
882
|
}
|
530
883
|
end
|
531
884
|
|
532
|
-
def
|
533
|
-
token = advance #
|
885
|
+
def parse_echo_statement
|
886
|
+
token = advance #Skip 'echo'
|
534
887
|
line = token[:line]
|
535
888
|
column = token[:column]
|
536
889
|
|
537
|
-
|
538
|
-
if @position < @tokens.length && current_token[:type] != :semicolon
|
539
|
-
value = parse_expression
|
540
|
-
end
|
890
|
+
expression = parse_expression
|
541
891
|
|
542
892
|
{
|
543
|
-
type: :
|
544
|
-
|
893
|
+
type: :echo_statement,
|
894
|
+
expression: expression,
|
545
895
|
line: line,
|
546
896
|
column: column
|
547
897
|
}
|
548
898
|
end
|
549
899
|
|
550
|
-
def
|
551
|
-
|
552
|
-
|
553
|
-
|
554
|
-
|
555
|
-
|
556
|
-
|
557
|
-
}
|
558
|
-
|
900
|
+
def parse_comment
|
901
|
+
original_token = current_token
|
902
|
+
line = original_token[:line]
|
903
|
+
column = original_token[:column]
|
904
|
+
comment = original_token[:value]
|
905
|
+
|
906
|
+
# puts "INSIDE PARSE COMMENT"
|
907
|
+
# puts "Current token: #{current_token.inspect}"
|
908
|
+
# puts "Peek token: #{peek_token.inspect}"
|
909
|
+
|
910
|
+
# Check if the comment contains a newline
|
911
|
+
if comment.include?("\n")
|
912
|
+
# Split the comment at the newline
|
913
|
+
parts = comment.split("\n", 2)
|
914
|
+
comment = parts[0] # Only use the part before newline
|
915
|
+
|
916
|
+
# Create a new token for the content after the newline
|
917
|
+
remainder = parts[1].strip
|
918
|
+
|
919
|
+
if !remainder.empty?
|
920
|
+
# Position correctly - we'll advance past the current token
|
921
|
+
# But should process the remainder separately later
|
922
|
+
|
923
|
+
# For debugging only, you can print what's being processed
|
924
|
+
# puts "Found comment with newline. Using: '#{comment}', Remainder: '#{remainder}'"
|
925
|
+
|
926
|
+
# Don't call advance() - we'll modify the current token instead
|
927
|
+
@tokens[@position] = {
|
928
|
+
type: :string, # Keep original type
|
929
|
+
value: comment, # Use only the part before newline
|
930
|
+
line: line,
|
931
|
+
column: column
|
932
|
+
}
|
559
933
|
|
560
|
-
|
561
|
-
|
934
|
+
# Insert the remainder as a new token after the current one
|
935
|
+
@tokens.insert(@position + 1, {
|
936
|
+
type: :comment, # Same type for consistency
|
937
|
+
value: remainder, # Preserve as a string token
|
938
|
+
line: line + 1, # Increment line number for the part after newline
|
939
|
+
column: 0 # Approximate column based on indentation
|
940
|
+
})
|
941
|
+
end
|
942
|
+
end
|
943
|
+
|
944
|
+
# Now advance past the (potentially modified) current token
|
945
|
+
advance
|
946
|
+
{ type: :comment, value: comment, line: line, column: column }
|
562
947
|
end
|
563
948
|
|
564
|
-
def
|
565
|
-
|
949
|
+
def parse_if_statement
|
950
|
+
token = advance # Skip 'if'
|
951
|
+
line = token[:line]
|
952
|
+
column = token[:column]
|
566
953
|
|
567
|
-
|
568
|
-
operator_precedence(current_token[:value]) >= precedence
|
569
|
-
op_token = advance
|
570
|
-
op = op_token[:value]
|
571
|
-
op_precedence = operator_precedence(op)
|
954
|
+
condition = parse_expression
|
572
955
|
|
573
|
-
|
956
|
+
then_branch = []
|
957
|
+
else_branch = []
|
574
958
|
|
575
|
-
|
576
|
-
|
577
|
-
operator: op,
|
578
|
-
left: left,
|
579
|
-
right: right,
|
580
|
-
line: op_token[:line],
|
581
|
-
column: op_token[:column]
|
582
|
-
}
|
583
|
-
end
|
959
|
+
# Check if this might be a one-line if (look ahead for pipe character)
|
960
|
+
one_line_if = false
|
584
961
|
|
585
|
-
|
586
|
-
|
962
|
+
if current_token && current_token[:type] == :operator && current_token[:value] == '|'
|
963
|
+
one_line_if = true
|
964
|
+
advance # Skip the pipe
|
587
965
|
|
588
|
-
|
589
|
-
|
590
|
-
|
591
|
-
|
592
|
-
|
593
|
-
|
594
|
-
|
595
|
-
|
596
|
-
|
597
|
-
|
598
|
-
|
599
|
-
|
600
|
-
|
601
|
-
|
602
|
-
|
603
|
-
|
604
|
-
|
605
|
-
|
966
|
+
# Parse the then statement
|
967
|
+
stmt = parse_statement
|
968
|
+
then_branch << stmt if stmt
|
969
|
+
|
970
|
+
# Check for the closing pipe and endif
|
971
|
+
if current_token && current_token[:type] == :operator && current_token[:value] == '|'
|
972
|
+
advance # Skip the pipe
|
973
|
+
|
974
|
+
# Expect endif
|
975
|
+
if current_token && current_token[:type] == :keyword && current_token[:value] == 'endif'
|
976
|
+
advance # Skip 'endif'
|
977
|
+
else
|
978
|
+
@errors << {
|
979
|
+
message: "Expected 'endif' after '|' in one-line if statement",
|
980
|
+
position: @position,
|
981
|
+
line: current_token ? current_token[:line] : 0,
|
982
|
+
column: current_token ? current_token[:column] : 0
|
983
|
+
}
|
984
|
+
end
|
985
|
+
end
|
986
|
+
else
|
987
|
+
# This is a regular multi-line if statement
|
988
|
+
# Continue with your existing logic for parsing normal if statements
|
989
|
+
|
990
|
+
# Parse statements until we hit 'else', 'elseif', or 'endif'
|
991
|
+
while @position < @tokens.length
|
992
|
+
# Check for the tokens that would terminate this block
|
993
|
+
if current_token && current_token[:type] == :keyword &&
|
994
|
+
['else', 'elseif', 'endif'].include?(current_token[:value])
|
995
|
+
break
|
996
|
+
end
|
997
|
+
|
998
|
+
stmt = parse_statement
|
999
|
+
then_branch << stmt if stmt
|
1000
|
+
end
|
1001
|
+
|
1002
|
+
# Check for else/elseif
|
1003
|
+
if current_token && current_token[:type] == :keyword
|
1004
|
+
if current_token[:value] == 'else'
|
1005
|
+
advance # Skip 'else'
|
1006
|
+
|
1007
|
+
# Parse statements until 'endif'
|
1008
|
+
while @position < @tokens.length
|
1009
|
+
if current_token[:type] == :keyword && current_token[:value] == 'endif'
|
1010
|
+
break
|
1011
|
+
end
|
1012
|
+
|
1013
|
+
stmt = parse_statement
|
1014
|
+
else_branch << stmt if stmt
|
1015
|
+
end
|
1016
|
+
elsif current_token[:value] == 'elseif'
|
1017
|
+
elseif_stmt = parse_if_statement
|
1018
|
+
else_branch << elseif_stmt if elseif_stmt
|
1019
|
+
|
1020
|
+
return {
|
1021
|
+
type: :if_statement,
|
1022
|
+
condition: condition,
|
1023
|
+
then_branch: then_branch,
|
1024
|
+
else_branch: else_branch,
|
1025
|
+
line: line,
|
1026
|
+
column: column
|
1027
|
+
}
|
1028
|
+
end
|
1029
|
+
end
|
1030
|
+
|
1031
|
+
# Expect endif
|
1032
|
+
if current_token && current_token[:type] == :keyword && current_token[:value] == 'endif'
|
1033
|
+
advance # Skip 'endif'
|
1034
|
+
else
|
1035
|
+
# Don't add an error if we've already reached the end of the file
|
1036
|
+
if @position < @tokens.length
|
1037
|
+
@errors << {
|
1038
|
+
message: "Expected 'endif' to close if statement",
|
1039
|
+
position: @position,
|
1040
|
+
line: current_token ? current_token[:line] : 0,
|
1041
|
+
column: current_token ? current_token[:column] : 0
|
1042
|
+
}
|
1043
|
+
end
|
1044
|
+
end
|
1045
|
+
end
|
1046
|
+
|
1047
|
+
{
|
1048
|
+
type: :if_statement,
|
1049
|
+
condition: condition,
|
1050
|
+
then_branch: then_branch,
|
1051
|
+
else_branch: else_branch,
|
1052
|
+
one_line: one_line_if,
|
1053
|
+
line: line,
|
1054
|
+
column: column
|
1055
|
+
}
|
1056
|
+
end
|
1057
|
+
|
1058
|
+
def parse_while_statement
|
1059
|
+
token = advance # Skip 'while'
|
1060
|
+
line = token[:line]
|
1061
|
+
column = token[:column]
|
1062
|
+
condition = parse_expression
|
1063
|
+
|
1064
|
+
body = []
|
1065
|
+
|
1066
|
+
# Parse statements until we hit 'endwhile'
|
1067
|
+
while @position < @tokens.length
|
1068
|
+
if current_token[:type] == :keyword && current_token[:value] == 'endwhile'
|
1069
|
+
break
|
1070
|
+
end
|
1071
|
+
|
1072
|
+
stmt = parse_statement
|
1073
|
+
body << stmt if stmt
|
1074
|
+
end
|
1075
|
+
|
1076
|
+
# Expect endwhile
|
1077
|
+
expect(:keyword) # This should be 'endwhile'
|
1078
|
+
|
1079
|
+
{
|
1080
|
+
type: :while_statement,
|
1081
|
+
condition: condition,
|
1082
|
+
body: body,
|
1083
|
+
line: line,
|
1084
|
+
column: column
|
1085
|
+
}
|
1086
|
+
end
|
1087
|
+
|
1088
|
+
def parse_for_statement
|
1089
|
+
token = advance # Skip 'for'
|
1090
|
+
line = token[:line]
|
1091
|
+
column = token[:column]
|
1092
|
+
|
1093
|
+
# Two main patterns:
|
1094
|
+
# 1. for [key, val] in dict - destructuring with bracket_open
|
1095
|
+
# 2. for var in list - simple variable with identifier
|
1096
|
+
|
1097
|
+
if current_token && current_token[:type] == :bracket_open
|
1098
|
+
# Handle destructuring assignment: for [key, val] in dict
|
1099
|
+
advance # Skip '['
|
1100
|
+
|
1101
|
+
loop_vars = []
|
1102
|
+
|
1103
|
+
loop do
|
1104
|
+
if current_token && (current_token[:type] == :identifier ||
|
1105
|
+
current_token[:type] == :local_variable ||
|
1106
|
+
current_token[:type] == :global_variable ||
|
1107
|
+
current_token[:type] == :script_local)
|
1108
|
+
loop_vars << advance
|
1109
|
+
else
|
1110
|
+
@errors << {
|
1111
|
+
message: "Expected identifier in for loop variables",
|
1112
|
+
position: @position,
|
1113
|
+
line: current_token ? current_token[:line] : 0,
|
1114
|
+
column: current_token ? current_token[:column] : 0
|
1115
|
+
}
|
1116
|
+
break
|
1117
|
+
end
|
1118
|
+
|
1119
|
+
if current_token && current_token[:type] == :comma
|
1120
|
+
advance # Skip ','
|
1121
|
+
else
|
1122
|
+
break
|
1123
|
+
end
|
1124
|
+
end
|
1125
|
+
|
1126
|
+
expect(:bracket_close) # Skip ']'
|
1127
|
+
|
1128
|
+
if !current_token || (current_token[:type] != :identifier || current_token[:value] != 'in')
|
1129
|
+
@errors << {
|
1130
|
+
message: "Expected 'in' after for loop variables",
|
1131
|
+
position: @position,
|
1132
|
+
line: current_token ? current_token[:line] : 0,
|
1133
|
+
column: current_token ? current_token[:column] : 0
|
1134
|
+
}
|
1135
|
+
else
|
1136
|
+
advance # Skip 'in'
|
1137
|
+
end
|
1138
|
+
|
1139
|
+
iterable = parse_expression
|
1140
|
+
|
1141
|
+
# Parse the body until 'endfor'
|
1142
|
+
body = []
|
1143
|
+
while @position < @tokens.length
|
1144
|
+
if current_token && current_token[:type] == :keyword && current_token[:value] == 'endfor'
|
1145
|
+
break
|
1146
|
+
end
|
1147
|
+
|
1148
|
+
stmt = parse_statement
|
1149
|
+
body << stmt if stmt
|
1150
|
+
end
|
1151
|
+
|
1152
|
+
# Expect endfor
|
1153
|
+
if current_token && current_token[:type] == :keyword && current_token[:value] == 'endfor'
|
1154
|
+
advance # Skip 'endfor'
|
1155
|
+
else
|
1156
|
+
# Only add an error if we haven't reached the end of the file
|
1157
|
+
if @position < @tokens.length
|
1158
|
+
@errors << {
|
1159
|
+
message: "Expected 'endfor' to close for statement",
|
1160
|
+
position: @position,
|
1161
|
+
line: current_token ? current_token[:line] : 0,
|
1162
|
+
column: current_token ? current_token[:column] : 0
|
1163
|
+
}
|
1164
|
+
end
|
1165
|
+
end
|
1166
|
+
|
1167
|
+
return {
|
1168
|
+
type: :for_statement,
|
1169
|
+
loop_vars: loop_vars,
|
1170
|
+
iterable: iterable,
|
1171
|
+
body: body,
|
1172
|
+
line: line,
|
1173
|
+
column: column
|
1174
|
+
}
|
1175
|
+
elsif current_token && current_token[:type] == :paren_open
|
1176
|
+
# Handle multiple variables in parentheses: for (var1, var2) in list
|
1177
|
+
advance # Skip '('
|
1178
|
+
|
1179
|
+
loop_vars = []
|
1180
|
+
|
1181
|
+
loop do
|
1182
|
+
if current_token && (current_token[:type] == :identifier ||
|
1183
|
+
current_token[:type] == :local_variable ||
|
1184
|
+
current_token[:type] == :global_variable ||
|
1185
|
+
current_token[:type] == :script_local)
|
1186
|
+
loop_vars << advance
|
1187
|
+
else
|
1188
|
+
@errors << {
|
1189
|
+
message: "Expected identifier in for loop variables",
|
1190
|
+
position: @position,
|
1191
|
+
line: current_token ? current_token[:line] : 0,
|
1192
|
+
column: current_token ? current_token[:column] : 0
|
1193
|
+
}
|
1194
|
+
break
|
1195
|
+
end
|
1196
|
+
|
1197
|
+
if current_token && current_token[:type] == :comma
|
1198
|
+
advance # Skip ','
|
1199
|
+
else
|
1200
|
+
break
|
1201
|
+
end
|
1202
|
+
end
|
1203
|
+
|
1204
|
+
expect(:paren_close) # Skip ')'
|
1205
|
+
|
1206
|
+
if !current_token || (current_token[:type] != :identifier || current_token[:value] != 'in')
|
1207
|
+
@errors << {
|
1208
|
+
message: "Expected 'in' after for loop variables",
|
1209
|
+
position: @position,
|
1210
|
+
line: current_token ? current_token[:line] : 0,
|
1211
|
+
column: current_token ? current_token[:column] : 0
|
1212
|
+
}
|
1213
|
+
else
|
1214
|
+
advance # Skip 'in'
|
1215
|
+
end
|
1216
|
+
|
1217
|
+
iterable = parse_expression
|
1218
|
+
|
1219
|
+
# Parse the body until 'endfor'
|
1220
|
+
body = []
|
1221
|
+
while @position < @tokens.length
|
1222
|
+
if current_token && current_token[:type] == :keyword && current_token[:value] == 'endfor'
|
1223
|
+
break
|
1224
|
+
end
|
1225
|
+
|
1226
|
+
stmt = parse_statement
|
1227
|
+
body << stmt if stmt
|
1228
|
+
end
|
1229
|
+
|
1230
|
+
# Expect endfor
|
1231
|
+
if current_token && current_token[:type] == :keyword && current_token[:value] == 'endfor'
|
1232
|
+
advance # Skip 'endfor'
|
1233
|
+
else
|
1234
|
+
# Only add an error if we haven't reached the end of the file
|
1235
|
+
if @position < @tokens.length
|
1236
|
+
@errors << {
|
1237
|
+
message: "Expected 'endfor' to close for statement",
|
1238
|
+
position: @position,
|
1239
|
+
line: current_token ? current_token[:line] : 0,
|
1240
|
+
column: current_token ? current_token[:column] : 0
|
1241
|
+
}
|
1242
|
+
end
|
1243
|
+
end
|
1244
|
+
|
1245
|
+
return {
|
1246
|
+
type: :for_statement,
|
1247
|
+
loop_vars: loop_vars,
|
1248
|
+
iterable: iterable,
|
1249
|
+
body: body,
|
1250
|
+
line: line,
|
1251
|
+
column: column
|
1252
|
+
}
|
1253
|
+
else
|
1254
|
+
# Handle single variable: for var in list
|
1255
|
+
if current_token && (current_token[:type] == :identifier ||
|
1256
|
+
current_token[:type] == :local_variable ||
|
1257
|
+
current_token[:type] == :global_variable ||
|
1258
|
+
current_token[:type] == :script_local)
|
1259
|
+
loop_var = advance
|
1260
|
+
else
|
1261
|
+
@errors << {
|
1262
|
+
message: "Expected identifier as for loop variable",
|
1263
|
+
position: @position,
|
1264
|
+
line: current_token ? current_token[:line] : 0,
|
1265
|
+
column: current_token ? current_token[:column] : 0
|
1266
|
+
}
|
1267
|
+
loop_var = nil
|
1268
|
+
end
|
1269
|
+
|
1270
|
+
if !current_token || (current_token[:type] != :identifier || current_token[:value] != 'in')
|
1271
|
+
@errors << {
|
1272
|
+
message: "Expected 'in' after for loop variable",
|
1273
|
+
position: @position,
|
1274
|
+
line: current_token ? current_token[:line] : 0,
|
1275
|
+
column: current_token ? current_token[:column] : 0
|
1276
|
+
}
|
1277
|
+
else
|
1278
|
+
advance # Skip 'in'
|
1279
|
+
end
|
1280
|
+
|
1281
|
+
iterable = parse_expression
|
1282
|
+
|
1283
|
+
# Parse the body until 'endfor'
|
1284
|
+
body = []
|
1285
|
+
while @position < @tokens.length
|
1286
|
+
if current_token && current_token[:type] == :keyword && current_token[:value] == 'endfor'
|
1287
|
+
break
|
1288
|
+
end
|
1289
|
+
|
1290
|
+
stmt = parse_statement
|
1291
|
+
body << stmt if stmt
|
1292
|
+
end
|
1293
|
+
|
1294
|
+
# Expect endfor
|
1295
|
+
if current_token && current_token[:type] == :keyword && current_token[:value] == 'endfor'
|
1296
|
+
advance # Skip 'endfor'
|
1297
|
+
else
|
1298
|
+
# Only add an error if we haven't reached the end of the file
|
1299
|
+
if @position < @tokens.length
|
1300
|
+
@errors << {
|
1301
|
+
message: "Expected 'endfor' to close for statement",
|
1302
|
+
position: @position,
|
1303
|
+
line: current_token ? current_token[:line] : 0,
|
1304
|
+
column: current_token ? current_token[:column] : 0
|
1305
|
+
}
|
1306
|
+
end
|
1307
|
+
end
|
1308
|
+
|
1309
|
+
return {
|
1310
|
+
type: :for_statement,
|
1311
|
+
loop_var: loop_var,
|
1312
|
+
iterable: iterable,
|
1313
|
+
body: body,
|
1314
|
+
line: line,
|
1315
|
+
column: column
|
1316
|
+
}
|
1317
|
+
end
|
1318
|
+
end
|
1319
|
+
|
1320
|
+
def parse_def_function
|
1321
|
+
token = advance # Skip 'def'
|
1322
|
+
line = token[:line]
|
1323
|
+
column = token[:column]
|
1324
|
+
|
1325
|
+
name = expect(:identifier)
|
1326
|
+
|
1327
|
+
# Parse parameter list
|
1328
|
+
expect(:paren_open)
|
1329
|
+
params = parse_parameter_list
|
1330
|
+
expect(:paren_close)
|
1331
|
+
|
1332
|
+
# Parse optional return type
|
1333
|
+
return_type = nil
|
1334
|
+
if current_token && current_token[:type] == :colon
|
1335
|
+
advance # Skip ':'
|
1336
|
+
return_type = parse_type
|
1337
|
+
end
|
1338
|
+
|
1339
|
+
# Parse function body
|
1340
|
+
body = []
|
1341
|
+
while @position < @tokens.length
|
1342
|
+
if current_token[:type] == :keyword && current_token[:value] == 'enddef'
|
1343
|
+
break
|
1344
|
+
end
|
1345
|
+
|
1346
|
+
stmt = parse_statement
|
1347
|
+
body << stmt if stmt
|
1348
|
+
end
|
1349
|
+
|
1350
|
+
# Expect enddef
|
1351
|
+
expect(:keyword) # This should be 'enddef'
|
1352
|
+
|
1353
|
+
{
|
1354
|
+
type: :def_function,
|
1355
|
+
name: name ? name[:value] : nil,
|
1356
|
+
params: params,
|
1357
|
+
return_type: return_type,
|
1358
|
+
body: body,
|
1359
|
+
line: line,
|
1360
|
+
column: column
|
1361
|
+
}
|
1362
|
+
end
|
1363
|
+
|
1364
|
+
def parse_parameter_list
|
1365
|
+
params = []
|
1366
|
+
|
1367
|
+
# Empty parameter list
|
1368
|
+
if current_token && current_token[:type] == :paren_close
|
1369
|
+
return params
|
1370
|
+
end
|
1371
|
+
|
1372
|
+
# Parse parameters until we find a closing parenthesis
|
1373
|
+
while @position < @tokens.length && current_token && current_token[:type] != :paren_close
|
1374
|
+
# Check for variable args
|
1375
|
+
if current_token && current_token[:type] == :ellipsis
|
1376
|
+
ellipsis_token = advance
|
1377
|
+
|
1378
|
+
# Parse type for variable args if present
|
1379
|
+
param_type = nil
|
1380
|
+
if current_token && current_token[:type] == :colon
|
1381
|
+
advance # Skip ':'
|
1382
|
+
param_type = parse_type
|
1383
|
+
end
|
1384
|
+
|
1385
|
+
params << {
|
1386
|
+
type: :var_args,
|
1387
|
+
param_type: param_type,
|
1388
|
+
line: ellipsis_token[:line],
|
1389
|
+
column: ellipsis_token[:column]
|
1390
|
+
}
|
1391
|
+
|
1392
|
+
# After varargs, we expect closing paren
|
1393
|
+
if current_token && current_token[:type] != :paren_close
|
1394
|
+
@errors << {
|
1395
|
+
message: "Expected closing parenthesis after varargs",
|
1396
|
+
position: @position,
|
1397
|
+
line: current_token[:line],
|
1398
|
+
column: current_token[:column]
|
1399
|
+
}
|
1400
|
+
end
|
1401
|
+
|
1402
|
+
break
|
1403
|
+
end
|
1404
|
+
|
1405
|
+
# Get parameter name
|
1406
|
+
if !current_token || current_token[:type] != :identifier
|
1407
|
+
@errors << {
|
1408
|
+
message: "Expected parameter name",
|
1409
|
+
position: @position,
|
1410
|
+
line: current_token ? current_token[:line] : 0,
|
1411
|
+
column: current_token ? current_token[:column] : 0
|
1412
|
+
}
|
1413
|
+
break
|
1414
|
+
end
|
1415
|
+
|
1416
|
+
param_name = advance
|
1417
|
+
|
1418
|
+
# Check for type annotation
|
1419
|
+
param_type = nil
|
1420
|
+
if current_token && current_token[:type] == :colon
|
1421
|
+
advance # Skip ':'
|
1422
|
+
param_type = parse_type
|
1423
|
+
end
|
1424
|
+
|
1425
|
+
# Check for default value
|
1426
|
+
default_value = nil
|
1427
|
+
if current_token && current_token[:type] == :operator && current_token[:value] == '='
|
1428
|
+
advance # Skip '='
|
1429
|
+
default_value = parse_expression
|
1430
|
+
end
|
1431
|
+
|
1432
|
+
params << {
|
1433
|
+
type: :parameter,
|
1434
|
+
name: param_name[:value],
|
1435
|
+
param_type: param_type,
|
1436
|
+
optional: default_value != nil,
|
1437
|
+
default_value: default_value,
|
1438
|
+
line: param_name[:line],
|
1439
|
+
column: param_name[:column]
|
1440
|
+
}
|
1441
|
+
|
1442
|
+
# If we have a comma, advance past it and continue
|
1443
|
+
if current_token && current_token[:type] == :comma
|
1444
|
+
advance
|
1445
|
+
# If we don't have a comma, we should have a closing paren
|
1446
|
+
elsif current_token && current_token[:type] != :paren_close
|
1447
|
+
@errors << {
|
1448
|
+
message: "Expected comma or closing parenthesis after parameter",
|
1449
|
+
position: @position,
|
1450
|
+
line: current_token[:line],
|
1451
|
+
column: current_token[:column]
|
1452
|
+
}
|
1453
|
+
break
|
1454
|
+
end
|
1455
|
+
end
|
1456
|
+
|
1457
|
+
params
|
1458
|
+
end
|
1459
|
+
|
1460
|
+
def parse_type
|
1461
|
+
if current_token && [:identifier, :keyword].include?(current_token[:type])
|
1462
|
+
type_name = advance
|
1463
|
+
|
1464
|
+
# Handle generic types like list<string>
|
1465
|
+
if current_token && current_token[:type] == :operator && current_token[:value] == '<'
|
1466
|
+
advance # Skip '<'
|
1467
|
+
inner_type = parse_type
|
1468
|
+
expect(:operator) # This should be '>'
|
1469
|
+
|
1470
|
+
return {
|
1471
|
+
type: :generic_type,
|
1472
|
+
base_type: type_name[:value],
|
1473
|
+
inner_type: inner_type,
|
1474
|
+
line: type_name[:line],
|
1475
|
+
column: type_name[:column]
|
1476
|
+
}
|
1477
|
+
end
|
1478
|
+
|
1479
|
+
return type_name[:value]
|
1480
|
+
else
|
1481
|
+
@errors << {
|
1482
|
+
message: "Expected type identifier",
|
1483
|
+
position: @position,
|
1484
|
+
line: current_token ? current_token[:line] : 0,
|
1485
|
+
column: current_token ? current_token[:column] : 0
|
1486
|
+
}
|
1487
|
+
advance
|
1488
|
+
return "unknown"
|
1489
|
+
end
|
1490
|
+
end
|
1491
|
+
|
1492
|
+
def parse_variable_declaration
|
1493
|
+
var_type_token = advance # Skip 'var', 'const', or 'final'
|
1494
|
+
var_type = var_type_token[:value]
|
1495
|
+
line = var_type_token[:line]
|
1496
|
+
column = var_type_token[:column]
|
1497
|
+
|
1498
|
+
if !current_token || current_token[:type] != :identifier
|
1499
|
+
@errors << {
|
1500
|
+
message: "Expected variable name",
|
1501
|
+
position: @position,
|
1502
|
+
line: current_token ? current_token[:line] : 0,
|
1503
|
+
column: current_token ? current_token[:column] : 0
|
1504
|
+
}
|
1505
|
+
return nil
|
1506
|
+
end
|
1507
|
+
|
1508
|
+
name_token = advance
|
1509
|
+
name = name_token[:value]
|
1510
|
+
|
1511
|
+
# Parse optional type annotation
|
1512
|
+
var_type_annotation = nil
|
1513
|
+
if current_token && current_token[:type] == :colon
|
1514
|
+
advance # Skip ':'
|
1515
|
+
var_type_annotation = parse_type
|
1516
|
+
end
|
1517
|
+
|
1518
|
+
# Parse initializer if present
|
1519
|
+
initializer = nil
|
1520
|
+
if current_token && current_token[:type] == :operator && current_token[:value] == '='
|
1521
|
+
advance # Skip '='
|
1522
|
+
initializer = parse_expression
|
1523
|
+
end
|
1524
|
+
|
1525
|
+
{
|
1526
|
+
type: :variable_declaration,
|
1527
|
+
var_type: var_type,
|
1528
|
+
name: name,
|
1529
|
+
var_type_annotation: var_type_annotation,
|
1530
|
+
initializer: initializer,
|
1531
|
+
line: line,
|
1532
|
+
column: column
|
1533
|
+
}
|
1534
|
+
end
|
1535
|
+
|
1536
|
+
def parse_return_statement
|
1537
|
+
token = advance # Skip 'return'
|
1538
|
+
line = token[:line]
|
1539
|
+
column = token[:column]
|
1540
|
+
|
1541
|
+
value = nil
|
1542
|
+
# Check if we've reached the end of the file, end of line, or a semicolon
|
1543
|
+
if @position < @tokens.length &&
|
1544
|
+
current_token &&
|
1545
|
+
current_token[:type] != :semicolon &&
|
1546
|
+
!(current_token[:type] == :keyword &&
|
1547
|
+
['endif', 'endwhile', 'endfor', 'endfunction', 'endfunc'].include?(current_token[:value]))
|
1548
|
+
value = parse_expression
|
1549
|
+
end
|
1550
|
+
|
1551
|
+
{
|
1552
|
+
type: :return_statement,
|
1553
|
+
value: value,
|
1554
|
+
line: line,
|
1555
|
+
column: column
|
1556
|
+
}
|
1557
|
+
end
|
1558
|
+
|
1559
|
+
def parse_expression_statement
|
1560
|
+
# Check if this is an assignment using a compound operator
|
1561
|
+
if current_token && current_token[:type] == :identifier
|
1562
|
+
variable_name = current_token[:value]
|
1563
|
+
variable_token = current_token
|
1564
|
+
advance # Move past the identifier
|
1565
|
+
|
1566
|
+
if current_token && current_token[:type] == :compound_operator
|
1567
|
+
operator = current_token[:value]
|
1568
|
+
operator_token = current_token
|
1569
|
+
advance # Move past the operator
|
1570
|
+
|
1571
|
+
right = parse_expression
|
1572
|
+
|
1573
|
+
return {
|
1574
|
+
type: :compound_assignment,
|
1575
|
+
operator: operator,
|
1576
|
+
target: {
|
1577
|
+
type: :identifier,
|
1578
|
+
name: variable_name,
|
1579
|
+
line: variable_token[:line],
|
1580
|
+
column: variable_token[:column]
|
1581
|
+
},
|
1582
|
+
value: right,
|
1583
|
+
line: operator_token[:line],
|
1584
|
+
column: operator_token[:column]
|
1585
|
+
}
|
1586
|
+
end
|
1587
|
+
|
1588
|
+
# If it wasn't a compound assignment, backtrack
|
1589
|
+
@position -= 1
|
1590
|
+
end
|
1591
|
+
|
1592
|
+
# Regular expression statement handling
|
1593
|
+
expr = parse_expression
|
1594
|
+
{
|
1595
|
+
type: :expression_statement,
|
1596
|
+
expression: expr,
|
1597
|
+
line: expr ? expr[:line] : 0,
|
1598
|
+
column: expr ? expr[:column] : 0
|
1599
|
+
}
|
1600
|
+
end
|
1601
|
+
|
1602
|
+
def parse_expression
|
1603
|
+
# binding.pry
|
1604
|
+
# Special case for empty return statements or standalone keywords that shouldn't be expressions
|
1605
|
+
if current_token && current_token[:type] == :keyword &&
|
1606
|
+
['return', 'endif', 'endwhile', 'endfor', 'endfunction', 'endfunc'].include?(current_token[:value])
|
1607
|
+
return nil
|
1608
|
+
end
|
1609
|
+
|
1610
|
+
if current_token[:type] == :string
|
1611
|
+
string_value = current_token[:value]
|
1612
|
+
while current_token && peek_token && [:line_continuation, :identifier].include?(peek_token[:type])
|
1613
|
+
# Handle strings with line continuation
|
1614
|
+
if ["'", '"'].include? current_token[:value][-1]
|
1615
|
+
token_line = current_token[:line]
|
1616
|
+
token_column = current_token[:column]
|
1617
|
+
advance # Consume the string token
|
1618
|
+
return {
|
1619
|
+
type: :literal,
|
1620
|
+
value: string_value,
|
1621
|
+
token_type: :string,
|
1622
|
+
line: token_line,
|
1623
|
+
column: token_column
|
1624
|
+
}
|
1625
|
+
else
|
1626
|
+
advance
|
1627
|
+
string_value += current_token[:value]
|
1628
|
+
end
|
1629
|
+
end
|
1630
|
+
end
|
1631
|
+
|
1632
|
+
# Parse the condition expression
|
1633
|
+
expr = parse_binary_expression
|
1634
|
+
|
1635
|
+
# Check if this is a ternary expression
|
1636
|
+
if current_token && (current_token[:type] == :question_mark || (current_token[:type] == :operator && current_token[:value] == '?'))
|
1637
|
+
question_token = advance # Skip '?'
|
1638
|
+
|
1639
|
+
# Parse the "then" expression
|
1640
|
+
then_expr = parse_expression
|
1641
|
+
|
1642
|
+
# Expect the colon
|
1643
|
+
if current_token && current_token[:type] == :colon
|
1644
|
+
colon_token = advance # Skip ':'
|
1645
|
+
|
1646
|
+
# Parse the "else" expression
|
1647
|
+
else_expr = parse_expression
|
1648
|
+
|
1649
|
+
# Return the ternary expression
|
1650
|
+
return {
|
1651
|
+
type: :ternary_expression,
|
1652
|
+
condition: expr,
|
1653
|
+
then_expr: then_expr,
|
1654
|
+
else_expr: else_expr,
|
1655
|
+
line: question_token[:line],
|
1656
|
+
column: question_token[:column]
|
1657
|
+
}
|
1658
|
+
else
|
1659
|
+
@errors << {
|
1660
|
+
message: "Expected ':' in ternary expression",
|
1661
|
+
position: @position,
|
1662
|
+
line: current_token ? current_token[:line] : 0,
|
1663
|
+
column: current_token ? current_token[:column] : 0
|
1664
|
+
}
|
1665
|
+
end
|
1666
|
+
end
|
1667
|
+
|
1668
|
+
return expr
|
1669
|
+
end
|
1670
|
+
|
1671
|
+
def parse_binary_expression(precedence = 0)
|
1672
|
+
left = parse_unary_expression
|
1673
|
+
|
1674
|
+
# Handle multi-line expressions where operators may appear at line beginnings
|
1675
|
+
while current_token &&
|
1676
|
+
(current_token[:type] == :operator ||
|
1677
|
+
(peek_token && peek_token[:type] == :operator && current_token[:type] == :whitespace))
|
1678
|
+
|
1679
|
+
# Skip any whitespace before the operator if it's at the beginning of a line
|
1680
|
+
if current_token[:type] == :whitespace
|
1681
|
+
advance
|
1682
|
+
end
|
1683
|
+
|
1684
|
+
if current_token && current_token[:type] == :operator && current_token[:value] == '.' &&
|
1685
|
+
operator_precedence(current_token[:value]) >= precedence
|
1686
|
+
op_token = advance # Skip the operator
|
1687
|
+
|
1688
|
+
# Check if we're dealing with a command placeholder on either side
|
1689
|
+
if (left && left[:type] == :command_arg_placeholder) ||
|
1690
|
+
(peek_token && peek_token[:type] == :command_arg_placeholder)
|
1691
|
+
# Handle command-specific concatenation
|
1692
|
+
right = parse_binary_expression(operator_precedence('.') + 1)
|
1693
|
+
|
1694
|
+
left = {
|
1695
|
+
type: :command_concat_expression, # Special type for command concatenation
|
1696
|
+
operator: '.',
|
1697
|
+
left: left,
|
1698
|
+
right: right,
|
1699
|
+
line: op_token[:line],
|
1700
|
+
column: op_token[:column]
|
1701
|
+
}
|
1702
|
+
else
|
1703
|
+
# Normal expression concatenation
|
1704
|
+
right = parse_binary_expression(operator_precedence('.') + 1)
|
1705
|
+
|
1706
|
+
left = {
|
1707
|
+
type: :binary_expression,
|
1708
|
+
operator: '.',
|
1709
|
+
left: left,
|
1710
|
+
right: right,
|
1711
|
+
line: op_token[:line],
|
1712
|
+
column: op_token[:column]
|
1713
|
+
}
|
1714
|
+
end
|
1715
|
+
elsif current_token && current_token[:type] == :operator &&
|
1716
|
+
['<', '>', '=', '!'].include?(current_token[:value]) &&
|
1717
|
+
peek_token && peek_token[:type] == :operator && peek_token[:value] == '='
|
1718
|
+
|
1719
|
+
# Combine the two operators into one token
|
1720
|
+
op_token = current_token
|
1721
|
+
op = current_token[:value] + peek_token[:value]
|
1722
|
+
advance # Skip the first operator
|
1723
|
+
advance # Skip the second operator
|
1724
|
+
|
1725
|
+
# Now process the combined operator
|
1726
|
+
op_precedence = operator_precedence(op)
|
1727
|
+
|
1728
|
+
if op_precedence >= precedence
|
1729
|
+
right = parse_binary_expression(op_precedence + 1)
|
1730
|
+
|
1731
|
+
left = {
|
1732
|
+
type: :binary_expression,
|
1733
|
+
operator: op,
|
1734
|
+
left: left,
|
1735
|
+
right: right,
|
1736
|
+
line: op_token[:line],
|
1737
|
+
column: op_token[:column]
|
1738
|
+
}
|
1739
|
+
end
|
1740
|
+
# Now we should be at the operator
|
1741
|
+
elsif current_token && current_token[:type] == :operator &&
|
1742
|
+
operator_precedence(current_token[:value]) >= precedence
|
1743
|
+
op_token = advance
|
1744
|
+
op = op_token[:value]
|
1745
|
+
op_precedence = operator_precedence(op)
|
1746
|
+
|
1747
|
+
right = parse_binary_expression(op_precedence + 1)
|
1748
|
+
|
1749
|
+
left = {
|
1750
|
+
type: :binary_expression,
|
1751
|
+
operator: op,
|
1752
|
+
left: left,
|
1753
|
+
right: right,
|
1754
|
+
line: op_token[:line],
|
1755
|
+
column: op_token[:column]
|
1756
|
+
}
|
1757
|
+
else
|
1758
|
+
break
|
1759
|
+
end
|
1760
|
+
end
|
1761
|
+
|
1762
|
+
return left
|
1763
|
+
end
|
1764
|
+
|
1765
|
+
def operator_precedence(op)
|
1766
|
+
case op
|
1767
|
+
when '..' # String concatenation
|
1768
|
+
1
|
1769
|
+
when '||' # Logical OR
|
1770
|
+
2
|
1771
|
+
when '&&' # Logical AND
|
1772
|
+
3
|
1773
|
+
when '==', '!=', '>', '<', '>=', '<=' # Comparison
|
1774
|
+
4
|
1775
|
+
when '+', '-' # Addition, subtraction
|
1776
|
+
5
|
1777
|
+
when '*', '/', '%' # Multiplication, division, modulo
|
1778
|
+
6
|
1779
|
+
when '.'
|
1780
|
+
7
|
1781
|
+
when '!'
|
1782
|
+
8
|
1783
|
+
else
|
1784
|
+
0
|
1785
|
+
end
|
1786
|
+
end
|
1787
|
+
|
1788
|
+
def parse_primary_expression
|
1789
|
+
return nil unless current_token
|
1790
|
+
|
1791
|
+
token = current_token
|
1792
|
+
line = token[:line]
|
1793
|
+
column = token[:column]
|
1794
|
+
|
1795
|
+
# First parse the basic expression
|
1796
|
+
expr = nil
|
1797
|
+
|
1798
|
+
case token[:type]
|
1799
|
+
# Add handling for command arg placeholders
|
1800
|
+
when :command_arg_placeholder
|
1801
|
+
advance
|
1802
|
+
expr = {
|
1803
|
+
type: :command_arg_placeholder,
|
1804
|
+
value: token[:value],
|
1805
|
+
line: line,
|
1806
|
+
column: column
|
1807
|
+
}
|
1808
|
+
# Add special handling for keywords that might appear in expressions
|
1809
|
+
when :keyword
|
1810
|
+
# Special handling for map-related keywords when they appear in expressions
|
1811
|
+
if ['map', 'nmap', 'imap', 'vmap', 'xmap', 'noremap', 'nnoremap', 'inoremap', 'vnoremap', 'xnoremap', 'cnoremap', 'cmap'].include?(token[:value])
|
1812
|
+
if peek_token[:type] == :paren_open
|
1813
|
+
parse_builtin_function_call(token[:value], line, column)
|
1814
|
+
else
|
1815
|
+
# Treat map commands as identifiers when inside expressions
|
1816
|
+
advance
|
1817
|
+
return {
|
1818
|
+
type: :identifier,
|
1819
|
+
name: token[:value],
|
1820
|
+
line: line,
|
1821
|
+
column: column
|
1822
|
+
}
|
1823
|
+
end
|
1824
|
+
elsif token[:value] == 'type' && current_token && (current_token[:type] == :paren_open || peek_token && peek_token[:type] == :paren_open)
|
1825
|
+
# This is the type() function call
|
1826
|
+
return parse_builtin_function_call(token[:value], line, column)
|
1827
|
+
elsif token[:value] == 'function'
|
1828
|
+
# Check if this is a function() call or just 'function' as property name
|
1829
|
+
if peek_token && peek_token[:type] == :paren_open
|
1830
|
+
advance # Skip 'function'
|
1831
|
+
# This is a function() call
|
1832
|
+
if current_token && current_token[:type] == :paren_open
|
1833
|
+
# This is a function reference call
|
1834
|
+
paren_token = advance # Skip '('
|
1835
|
+
|
1836
|
+
# Parse the function name as a string or arrow function
|
1837
|
+
if current_token && current_token[:type] == :string
|
1838
|
+
func_name = current_token[:value]
|
1839
|
+
advance
|
1840
|
+
|
1841
|
+
# Check for additional arguments (function() can take multiple arguments)
|
1842
|
+
args = []
|
1843
|
+
while current_token && current_token[:type] == :comma
|
1844
|
+
advance # Skip comma
|
1845
|
+
arg = parse_expression
|
1846
|
+
args << arg if arg
|
1847
|
+
end
|
1848
|
+
|
1849
|
+
# Expect closing parenthesis
|
1850
|
+
expect(:paren_close) # Skip ')'
|
1851
|
+
|
1852
|
+
return {
|
1853
|
+
type: :function_reference,
|
1854
|
+
function_name: func_name,
|
1855
|
+
arguments: args,
|
1856
|
+
line: line,
|
1857
|
+
column: column
|
1858
|
+
}
|
1859
|
+
elsif current_token && current_token[:type] == :brace_open
|
1860
|
+
# This is an arrow function inside function()
|
1861
|
+
arrow_function = parse_vim_lambda(line, column)
|
1862
|
+
|
1863
|
+
# Expect closing parenthesis
|
1864
|
+
expect(:paren_close) # Skip ')'
|
1865
|
+
|
1866
|
+
return {
|
1867
|
+
type: :function_reference,
|
1868
|
+
function_body: arrow_function,
|
1869
|
+
line: line,
|
1870
|
+
column: column
|
1871
|
+
}
|
1872
|
+
else
|
1873
|
+
@errors << {
|
1874
|
+
message: "Expected string or arrow function definition in function() call",
|
1875
|
+
position: @position,
|
1876
|
+
line: current_token ? current_token[:line] : 0,
|
1877
|
+
column: current_token ? current_token[:column] : 0
|
1878
|
+
}
|
1879
|
+
end
|
1880
|
+
end
|
1881
|
+
else
|
1882
|
+
# If not followed by parenthesis, treat 'function' as an identifier (property name)
|
1883
|
+
advance # Skip 'function'
|
1884
|
+
return {
|
1885
|
+
type: :identifier,
|
1886
|
+
name: token[:value],
|
1887
|
+
line: line,
|
1888
|
+
column: column
|
1889
|
+
}
|
1890
|
+
end
|
1891
|
+
# Legacy Vim allows certain keywords as identifiers in expressions
|
1892
|
+
elsif ['return', 'type'].include?(token[:value])
|
1893
|
+
# Handle 'return' keyword specially when it appears in an expression context
|
1894
|
+
advance
|
1895
|
+
@warnings << {
|
1896
|
+
message: "Keyword '#{token[:value]}' used in an expression context",
|
1897
|
+
position: @position,
|
1898
|
+
line: line,
|
1899
|
+
column: column
|
1900
|
+
}
|
1901
|
+
# Check if this is a function call for 'type'
|
1902
|
+
if token[:value] == 'type' && current_token && current_token[:type] == :paren_open
|
1903
|
+
return parse_function_call(token[:value], line, column)
|
1904
|
+
end
|
1905
|
+
|
1906
|
+
expr = {
|
1907
|
+
type: :identifier,
|
1908
|
+
name: token[:value],
|
1909
|
+
line: line,
|
1910
|
+
column: column
|
1911
|
+
}
|
1912
|
+
else
|
1913
|
+
@errors << {
|
1914
|
+
message: "Unexpected keyword in expression: #{token[:value]}",
|
1915
|
+
position: @position,
|
1916
|
+
line: line,
|
1917
|
+
column: column
|
1918
|
+
}
|
1919
|
+
advance
|
1920
|
+
return nil
|
1921
|
+
end
|
1922
|
+
when :number
|
1923
|
+
advance
|
1924
|
+
expr = {
|
1925
|
+
type: :literal,
|
1926
|
+
value: token[:value],
|
1927
|
+
token_type: :number,
|
1928
|
+
line: line,
|
1929
|
+
column: column
|
1930
|
+
}
|
1931
|
+
when :string
|
1932
|
+
# parse_string
|
1933
|
+
if token[:value].start_with?('"')
|
1934
|
+
advance
|
1935
|
+
return {
|
1936
|
+
type: :comment,
|
1937
|
+
value: token[:value],
|
1938
|
+
line: line,
|
1939
|
+
column: column
|
1940
|
+
}
|
1941
|
+
else
|
1942
|
+
string_value = token[:value]
|
1943
|
+
advance
|
1944
|
+
expr = {
|
1945
|
+
type: :literal,
|
1946
|
+
value: string_value,
|
1947
|
+
raw_value: string_value, # Store the raw string to preserve escapes
|
1948
|
+
token_type: :string,
|
1949
|
+
line: line,
|
1950
|
+
column: column
|
1951
|
+
}
|
1952
|
+
end
|
1953
|
+
when :option_variable
|
1954
|
+
# Handle Vim option variables (like &compatible)
|
1955
|
+
advance
|
1956
|
+
expr = {
|
1957
|
+
type: :option_variable,
|
1958
|
+
name: token[:value],
|
1959
|
+
line: line,
|
1960
|
+
column: column
|
1961
|
+
}
|
1962
|
+
when :scoped_option_variable
|
1963
|
+
advance
|
1964
|
+
expr = {
|
1965
|
+
type: :scoped_option_variable,
|
1966
|
+
name: token[:value],
|
1967
|
+
line: line,
|
1968
|
+
column: column
|
1969
|
+
}
|
1970
|
+
when :special_variable
|
1971
|
+
# Handle Vim special variables (like v:version)
|
1972
|
+
advance
|
1973
|
+
expr = {
|
1974
|
+
type: :special_variable,
|
1975
|
+
name: token[:value],
|
1976
|
+
line: line,
|
1977
|
+
column: column
|
1978
|
+
}
|
1979
|
+
when :script_local
|
1980
|
+
# Handle script-local variables/functions (like s:var)
|
1981
|
+
advance
|
1982
|
+
|
1983
|
+
# Check if this is a function call
|
1984
|
+
if current_token && current_token[:type] == :paren_open
|
1985
|
+
return parse_function_call(token[:value], line, column)
|
1986
|
+
end
|
1987
|
+
|
1988
|
+
expr = {
|
1989
|
+
type: :script_local,
|
1990
|
+
name: token[:value],
|
1991
|
+
line: line,
|
1992
|
+
column: column
|
1993
|
+
}
|
1994
|
+
when :buffer_local
|
1995
|
+
# Handle script-local variables/functions (like s:var)
|
1996
|
+
advance
|
1997
|
+
|
1998
|
+
# Check if this is a function call
|
1999
|
+
if current_token && current_token[:type] == :paren_open
|
2000
|
+
return parse_function_call(token[:value], line, column)
|
2001
|
+
end
|
2002
|
+
|
2003
|
+
expr = {
|
2004
|
+
type: :buffer_local,
|
2005
|
+
name: token[:value],
|
2006
|
+
line: line,
|
2007
|
+
column: column
|
2008
|
+
}
|
2009
|
+
when :global_variable
|
2010
|
+
# Handle global variables (like g:var)
|
2011
|
+
advance
|
2012
|
+
expr = {
|
2013
|
+
type: :global_variable,
|
2014
|
+
name: token[:value],
|
2015
|
+
line: line,
|
2016
|
+
column: column
|
2017
|
+
}
|
2018
|
+
when :arg_variable
|
2019
|
+
# Handle function argument variables (like a:var)
|
2020
|
+
advance
|
2021
|
+
expr = {
|
2022
|
+
type: :arg_variable,
|
2023
|
+
name: token[:value],
|
2024
|
+
line: line,
|
2025
|
+
column: column
|
2026
|
+
}
|
2027
|
+
when :local_variable
|
2028
|
+
# Handle local variables (like l:var)
|
2029
|
+
advance
|
2030
|
+
expr = {
|
2031
|
+
type: :local_variable,
|
2032
|
+
name: token[:value],
|
2033
|
+
line: line,
|
2034
|
+
column: column
|
2035
|
+
}
|
2036
|
+
when :identifier
|
2037
|
+
# Special handling for Vim built-in functions
|
2038
|
+
if ['has', 'exists', 'empty', 'get', 'type', 'map', 'copy'].include?(token[:value])
|
2039
|
+
expr = parse_builtin_function_call(token[:value], line, column)
|
2040
|
+
else
|
2041
|
+
advance
|
2042
|
+
|
2043
|
+
# Check if this is a function call
|
2044
|
+
if current_token && current_token[:type] == :paren_open
|
2045
|
+
expr = parse_function_call(token[:value], line, column)
|
2046
|
+
else
|
2047
|
+
# Special handling for execute command
|
2048
|
+
if token[:value] == 'execute'
|
2049
|
+
# Parse the string expressions for execute
|
2050
|
+
# For now we'll just treat it as a normal identifier
|
2051
|
+
expr = {
|
2052
|
+
type: :identifier,
|
2053
|
+
name: token[:value],
|
2054
|
+
line: line,
|
2055
|
+
column: column
|
2056
|
+
}
|
2057
|
+
else
|
2058
|
+
expr = {
|
2059
|
+
type: :identifier,
|
2060
|
+
name: token[:value],
|
2061
|
+
line: line,
|
2062
|
+
column: column
|
2063
|
+
}
|
2064
|
+
end
|
2065
|
+
end
|
2066
|
+
end
|
2067
|
+
when :namespace_prefix
|
2068
|
+
advance
|
2069
|
+
expr = {
|
2070
|
+
type: :namespace_prefix,
|
2071
|
+
name: token[:value],
|
2072
|
+
line: line,
|
2073
|
+
column: column
|
2074
|
+
}
|
2075
|
+
when :paren_open
|
2076
|
+
if is_lambda_expression
|
2077
|
+
return parse_lambda_expression(line, column)
|
2078
|
+
else
|
2079
|
+
advance # Skip '('
|
2080
|
+
expr = parse_expression
|
2081
|
+
expect(:paren_close) # Expect and skip ')'
|
2082
|
+
end
|
2083
|
+
when :bracket_open
|
2084
|
+
expr = parse_list_literal(line, column)
|
2085
|
+
when :brace_open
|
2086
|
+
expr = parse_vim_lambda_or_dict(line, column)
|
2087
|
+
when :backslash
|
2088
|
+
# Handle line continuation with backslash
|
2089
|
+
advance
|
2090
|
+
expr = parse_expression
|
2091
|
+
when :register_access
|
2092
|
+
advance
|
2093
|
+
expr = {
|
2094
|
+
type: :register_access,
|
2095
|
+
register: token[:value][1..-1], # Remove the @ symbol
|
2096
|
+
line: line,
|
2097
|
+
column: column
|
2098
|
+
}
|
2099
|
+
when :line_continuation
|
2100
|
+
advance
|
2101
|
+
expr = parse_expression
|
2102
|
+
else
|
2103
|
+
@errors << {
|
2104
|
+
message: "Unexpected token in expression: #{token[:type]}",
|
2105
|
+
position: @position,
|
2106
|
+
line: line,
|
2107
|
+
column: column
|
2108
|
+
}
|
2109
|
+
advance
|
2110
|
+
return nil
|
2111
|
+
end
|
2112
|
+
|
2113
|
+
# Now handle any chained property access or method calls
|
2114
|
+
while current_token
|
2115
|
+
# Check for property access with dot
|
2116
|
+
if current_token[:type] == :operator && current_token[:value] == '.'
|
2117
|
+
# Look ahead to determine if this is property access or concatenation
|
2118
|
+
next_token = peek_token
|
2119
|
+
is_property_access = next_token && (next_token[:type] == :identifier || next_token[:type] == :keyword)
|
2120
|
+
|
2121
|
+
# Check if this is a property access (only when right side is identifier/keyword)
|
2122
|
+
if is_property_access && (expr[:type] == :identifier || expr[:type] == :global_variable ||
|
2123
|
+
expr[:type] == :script_local || expr[:type] == :namespace_prefix ||
|
2124
|
+
expr[:type] == :arg_variable || expr[:type] == :buffer_local ||
|
2125
|
+
expr[:type] == :window_local || expr[:type] == :tab_local ||
|
2126
|
+
expr[:type] == :local_variable || expr[:type] == :property_access ||
|
2127
|
+
expr[:type] == :indexed_access)
|
2128
|
+
|
2129
|
+
dot_token = advance # Skip '.'
|
2130
|
+
# Next token should be an identifier (property name)
|
2131
|
+
if !current_token || (current_token[:type] != :identifier &&
|
2132
|
+
current_token[:type] != :keyword)
|
2133
|
+
@errors << {
|
2134
|
+
message: "Expected property name after '.'",
|
2135
|
+
position: @position,
|
2136
|
+
line: current_token ? current_token[:line] : 0,
|
2137
|
+
column: current_token ? current_token[:column] : 0
|
2138
|
+
}
|
2139
|
+
break
|
2140
|
+
end
|
2141
|
+
property_token = advance
|
2142
|
+
|
2143
|
+
expr = {
|
2144
|
+
type: :property_access,
|
2145
|
+
object: expr,
|
2146
|
+
property: property_token[:value],
|
2147
|
+
line: dot_token[:line],
|
2148
|
+
column: dot_token[:column]
|
2149
|
+
}
|
2150
|
+
else
|
2151
|
+
# This is concatenation or other binary operation, break to let binary expression parser handle it
|
2152
|
+
break
|
2153
|
+
end
|
2154
|
+
|
2155
|
+
# Check for method call with arrow ->
|
2156
|
+
elsif current_token[:type] == :operator && current_token[:value] == '->'
|
2157
|
+
arrow_token = advance # Skip '->'
|
2158
|
+
|
2159
|
+
# Next token should be an identifier (method name)
|
2160
|
+
if !current_token || current_token[:type] != :identifier
|
2161
|
+
@errors << {
|
2162
|
+
message: "Expected method name after '->'",
|
2163
|
+
position: @position,
|
2164
|
+
line: current_token ? current_token[:line] : 0,
|
2165
|
+
column: current_token ? current_token[:column] : 0
|
2166
|
+
}
|
2167
|
+
break
|
2168
|
+
end
|
2169
|
+
|
2170
|
+
method_name = advance[:value] # Get method name
|
2171
|
+
|
2172
|
+
# Check for arguments
|
2173
|
+
args = []
|
2174
|
+
if current_token && current_token[:type] == :paren_open
|
2175
|
+
expect(:paren_open) # Skip '('
|
2176
|
+
|
2177
|
+
# Parse arguments if any
|
2178
|
+
unless current_token && current_token[:type] == :paren_close
|
2179
|
+
loop do
|
2180
|
+
arg = parse_expression
|
2181
|
+
args << arg if arg
|
2182
|
+
|
2183
|
+
if current_token && current_token[:type] == :comma
|
2184
|
+
advance # Skip comma
|
2185
|
+
else
|
2186
|
+
break
|
2187
|
+
end
|
2188
|
+
end
|
2189
|
+
end
|
2190
|
+
|
2191
|
+
expect(:paren_close) # Skip ')'
|
2192
|
+
end
|
2193
|
+
|
2194
|
+
expr = {
|
2195
|
+
type: :method_call,
|
2196
|
+
object: expr,
|
2197
|
+
method: method_name,
|
2198
|
+
arguments: args,
|
2199
|
+
line: arrow_token[:line],
|
2200
|
+
column: arrow_token[:column]
|
2201
|
+
}
|
2202
|
+
# Check for indexing with brackets
|
2203
|
+
elsif current_token[:type] == :bracket_open
|
2204
|
+
bracket_token = advance # Skip '['
|
2205
|
+
index_expr = parse_expression
|
2206
|
+
|
2207
|
+
# Add support for list slicing with colon
|
2208
|
+
end_index = nil
|
2209
|
+
if current_token && current_token[:type] == :colon
|
2210
|
+
advance # Skip ':'
|
2211
|
+
# handle omitted end index case like [6:]
|
2212
|
+
if current_token && current_token[:type] == :bracket_close
|
2213
|
+
end_index = {
|
2214
|
+
type: :implicit_end_index,
|
2215
|
+
value: nil,
|
2216
|
+
line: current_token[:line],
|
2217
|
+
column: current_token[:column]
|
2218
|
+
}
|
2219
|
+
else
|
2220
|
+
end_index = parse_expression
|
2221
|
+
end
|
2222
|
+
end
|
2223
|
+
|
2224
|
+
expect(:bracket_close) # Skip ']'
|
2225
|
+
|
2226
|
+
if end_index
|
2227
|
+
# This is a slice operation
|
2228
|
+
expr = {
|
2229
|
+
type: :slice_access,
|
2230
|
+
object: expr,
|
2231
|
+
start_index: index_expr,
|
2232
|
+
end_index: end_index,
|
2233
|
+
line: bracket_token[:line],
|
2234
|
+
column: bracket_token[:column]
|
2235
|
+
}
|
2236
|
+
else
|
2237
|
+
# Regular indexed access
|
2238
|
+
expr = {
|
2239
|
+
type: :indexed_access,
|
2240
|
+
object: expr,
|
2241
|
+
index: index_expr,
|
2242
|
+
line: bracket_token[:line],
|
2243
|
+
column: bracket_token[:column]
|
2244
|
+
}
|
2245
|
+
end
|
2246
|
+
# Check for function call directly on an expression
|
2247
|
+
elsif current_token[:type] == :paren_open
|
2248
|
+
paren_token = advance # Skip '('
|
2249
|
+
|
2250
|
+
args = []
|
2251
|
+
|
2252
|
+
# Parse arguments if any
|
2253
|
+
unless current_token && current_token[:type] == :paren_close
|
2254
|
+
loop do
|
2255
|
+
arg = parse_expression
|
2256
|
+
args << arg if arg
|
2257
|
+
|
2258
|
+
if current_token && current_token[:type] == :comma
|
2259
|
+
advance # Skip comma
|
2260
|
+
else
|
2261
|
+
break
|
2262
|
+
end
|
2263
|
+
end
|
2264
|
+
end
|
2265
|
+
|
2266
|
+
expect(:paren_close) # Skip ')'
|
2267
|
+
|
2268
|
+
expr = {
|
2269
|
+
type: :call_expression,
|
2270
|
+
callee: expr,
|
2271
|
+
arguments: args,
|
2272
|
+
line: paren_token[:line],
|
2273
|
+
column: paren_token[:column]
|
2274
|
+
}
|
2275
|
+
else
|
2276
|
+
# No more chaining
|
2277
|
+
break
|
2278
|
+
end
|
2279
|
+
end
|
2280
|
+
|
2281
|
+
return expr
|
2282
|
+
end
|
2283
|
+
|
2284
|
+
def parse_builtin_function_call(name, line, column)
|
2285
|
+
# Skip the function name (already consumed)
|
2286
|
+
advance if current_token[:value] == name
|
2287
|
+
|
2288
|
+
# Check if there's an opening parenthesis
|
2289
|
+
if current_token && current_token[:type] == :paren_open
|
2290
|
+
advance # Skip '('
|
2291
|
+
|
2292
|
+
# Parse arguments
|
2293
|
+
args = []
|
2294
|
+
|
2295
|
+
# Functions that take string expressions as code
|
2296
|
+
special_functions = ['map', 'reduce', 'sort', 'call', 'eval', 'execute', 'exec']
|
2297
|
+
is_special_function = special_functions.include?(name)
|
2298
|
+
|
2299
|
+
# Parse until closing parenthesis
|
2300
|
+
while @position < @tokens.length && current_token && current_token[:type] != :paren_close
|
2301
|
+
# Skip whitespace or comments
|
2302
|
+
if current_token[:type] == :whitespace || current_token[:type] == :comment
|
2303
|
+
advance
|
2304
|
+
next
|
2305
|
+
end
|
2306
|
+
|
2307
|
+
# Special handling for string arguments that contain code
|
2308
|
+
if is_special_function &&
|
2309
|
+
current_token && current_token[:type] == :string
|
2310
|
+
string_token = parse_expression
|
2311
|
+
args << {
|
2312
|
+
type: :literal,
|
2313
|
+
value: string_token[:value],
|
2314
|
+
token_type: :string,
|
2315
|
+
line: string_token[:line],
|
2316
|
+
column: string_token[:column]
|
2317
|
+
}
|
2318
|
+
else
|
2319
|
+
arg = parse_expression
|
2320
|
+
args << arg if arg
|
2321
|
+
end
|
2322
|
+
|
2323
|
+
if current_token && current_token[:type] == :comma
|
2324
|
+
advance
|
2325
|
+
elsif current_token && current_token[:type] != :paren_close
|
2326
|
+
@errors << {
|
2327
|
+
message: "Expected comma or closing parenthesis in #{name} function",
|
2328
|
+
position: @position,
|
2329
|
+
line: current_token[:line],
|
2330
|
+
column: current_token[:column]
|
2331
|
+
}
|
2332
|
+
break
|
2333
|
+
end
|
2334
|
+
end
|
2335
|
+
|
2336
|
+
# Check for closing parenthesis
|
2337
|
+
if current_token && current_token[:type] == :paren_close
|
2338
|
+
advance # Skip ')'
|
2339
|
+
else
|
2340
|
+
@errors << {
|
2341
|
+
message: "Expected ')' to close #{name} function call",
|
2342
|
+
position: @position,
|
2343
|
+
line: current_token ? current_token[:line] : 0,
|
2344
|
+
column: current_token ? current_token[:column] : 0
|
2345
|
+
}
|
2346
|
+
end
|
2347
|
+
else
|
2348
|
+
# Handle legacy Vim script where parentheses might be omitted
|
2349
|
+
# Just parse one expression as the argument
|
2350
|
+
args = [parse_expression]
|
2351
|
+
end
|
2352
|
+
|
2353
|
+
# Return function call node
|
2354
|
+
{
|
2355
|
+
type: :builtin_function_call,
|
2356
|
+
name: name,
|
2357
|
+
arguments: args,
|
2358
|
+
line: line,
|
2359
|
+
column: column
|
2360
|
+
}
|
2361
|
+
end
|
2362
|
+
|
2363
|
+
def is_lambda_expression
|
2364
|
+
# Save the current position
|
2365
|
+
original_position = @position
|
2366
|
+
|
2367
|
+
# Skip the opening parenthesis
|
2368
|
+
advance if current_token && current_token[:type] == :paren_open
|
2369
|
+
|
2370
|
+
# Check for a parameter list followed by => or ): =>
|
2371
|
+
has_params = false
|
2372
|
+
has_arrow = false
|
2373
|
+
|
2374
|
+
# Skip past parameters
|
2375
|
+
loop do
|
2376
|
+
break if !current_token
|
2377
|
+
|
2378
|
+
if current_token[:type] == :identifier
|
2379
|
+
has_params = true
|
2380
|
+
advance
|
2381
|
+
|
2382
|
+
# Either comma for another parameter, or close paren
|
2383
|
+
if current_token && current_token[:type] == :comma
|
2384
|
+
advance
|
2385
|
+
elsif current_token && current_token[:type] == :paren_close
|
2386
|
+
advance
|
2387
|
+
break
|
2388
|
+
else
|
2389
|
+
# Not a valid lambda parameter list
|
2390
|
+
break
|
2391
|
+
end
|
2392
|
+
elsif current_token[:type] == :paren_close
|
2393
|
+
# Empty parameter list is valid
|
2394
|
+
advance
|
2395
|
+
break
|
2396
|
+
else
|
2397
|
+
# Not a valid lambda parameter list
|
2398
|
+
break
|
2399
|
+
end
|
2400
|
+
end
|
2401
|
+
|
2402
|
+
# After parameters, check for either => or ): =>
|
2403
|
+
if current_token
|
2404
|
+
if current_token[:type] == :operator && current_token[:value] == '=>'
|
2405
|
+
has_arrow = true
|
2406
|
+
elsif current_token[:type] == :colon
|
2407
|
+
# There might be a return type annotation
|
2408
|
+
advance
|
2409
|
+
# Skip the type
|
2410
|
+
advance if current_token && [:identifier, :keyword].include?(current_token[:type])
|
2411
|
+
# Check for the arrow
|
2412
|
+
has_arrow = current_token && current_token[:type] == :operator && current_token[:value] == '=>'
|
2413
|
+
end
|
2414
|
+
end
|
2415
|
+
|
2416
|
+
# Reset position to where we started
|
2417
|
+
@position = original_position
|
2418
|
+
|
2419
|
+
return has_params && has_arrow
|
2420
|
+
end
|
2421
|
+
|
2422
|
+
def parse_vim_lambda(line, column)
|
2423
|
+
advance # Skip opening brace
|
2424
|
+
|
2425
|
+
# Parse parameters
|
2426
|
+
params = []
|
2427
|
+
|
2428
|
+
# Check if this is a no-parameter lambda (starts with ->)
|
2429
|
+
if current_token && current_token[:type] == :operator && current_token[:value] == '->'
|
2430
|
+
# No parameters, skip to arrow parsing
|
2431
|
+
else
|
2432
|
+
# Parse parameter(s)
|
2433
|
+
if current_token && (current_token[:type] == :identifier ||
|
2434
|
+
current_token[:type] == :local_variable ||
|
2435
|
+
current_token[:type] == :global_variable)
|
2436
|
+
param_name = current_token[:value]
|
2437
|
+
param_line = current_token[:line]
|
2438
|
+
param_column = current_token[:column]
|
2439
|
+
advance
|
2440
|
+
|
2441
|
+
params << {
|
2442
|
+
type: :parameter,
|
2443
|
+
name: param_name,
|
2444
|
+
line: param_line,
|
2445
|
+
column: param_column
|
2446
|
+
}
|
2447
|
+
|
2448
|
+
# Handle multiple parameters (comma-separated)
|
2449
|
+
while current_token && current_token[:type] == :comma
|
2450
|
+
advance # Skip comma
|
2451
|
+
|
2452
|
+
if current_token && (current_token[:type] == :identifier ||
|
2453
|
+
current_token[:type] == :local_variable ||
|
2454
|
+
current_token[:type] == :global_variable)
|
2455
|
+
param_name = current_token[:value]
|
2456
|
+
param_line = current_token[:line]
|
2457
|
+
param_column = current_token[:column]
|
2458
|
+
advance
|
2459
|
+
|
2460
|
+
params << {
|
2461
|
+
type: :parameter,
|
2462
|
+
name: param_name,
|
2463
|
+
line: param_line,
|
2464
|
+
column: param_column
|
2465
|
+
}
|
2466
|
+
else
|
2467
|
+
@errors << {
|
2468
|
+
message: "Expected parameter name after comma in lambda function",
|
2469
|
+
position: @position,
|
2470
|
+
line: current_token ? current_token[:line] : 0,
|
2471
|
+
column: current_token ? current_token[:column] : 0
|
2472
|
+
}
|
2473
|
+
break
|
2474
|
+
end
|
2475
|
+
end
|
2476
|
+
else
|
2477
|
+
# For no-parameter lambdas, this is not an error
|
2478
|
+
if !(current_token && current_token[:type] == :operator && current_token[:value] == '->')
|
2479
|
+
@errors << {
|
2480
|
+
message: "Expected parameter name or '->' in lambda function",
|
2481
|
+
position: @position,
|
2482
|
+
line: current_token ? current_token[:line] : 0,
|
2483
|
+
column: current_token ? current_token[:column] : 0
|
2484
|
+
}
|
2485
|
+
end
|
2486
|
+
end
|
2487
|
+
end
|
2488
|
+
|
2489
|
+
# Expect the arrow token
|
2490
|
+
if current_token && current_token[:type] == :operator && current_token[:value] == '->'
|
2491
|
+
advance # Skip ->
|
2492
|
+
else
|
2493
|
+
@errors << {
|
2494
|
+
message: "Expected '->' in lambda function",
|
2495
|
+
position: @position,
|
2496
|
+
line: current_token ? current_token[:line] : 0,
|
2497
|
+
column: current_token ? current_token[:column] : 0
|
2498
|
+
}
|
2499
|
+
end
|
2500
|
+
|
2501
|
+
# Parse the lambda body expression (everything until the closing brace)
|
2502
|
+
body = parse_expression
|
2503
|
+
|
2504
|
+
# Expect closing brace
|
2505
|
+
if current_token && current_token[:type] == :brace_close
|
2506
|
+
advance # Skip }
|
2507
|
+
else
|
2508
|
+
@errors << {
|
2509
|
+
message: "Expected closing brace for lambda function",
|
2510
|
+
position: @position,
|
2511
|
+
line: current_token ? current_token[:line] : 0,
|
2512
|
+
column: current_token ? current_token[:column] : 0
|
2513
|
+
}
|
2514
|
+
end
|
2515
|
+
|
2516
|
+
return {
|
2517
|
+
type: :vim_lambda,
|
2518
|
+
params: params,
|
2519
|
+
body: body,
|
2520
|
+
line: line,
|
2521
|
+
column: column
|
2522
|
+
}
|
2523
|
+
end
|
2524
|
+
|
2525
|
+
def parse_lambda_expression(line, column)
|
2526
|
+
expect(:paren_open) # Skip '('
|
2527
|
+
|
2528
|
+
params = []
|
2529
|
+
|
2530
|
+
# Parse parameters
|
2531
|
+
unless current_token && current_token[:type] == :paren_close
|
2532
|
+
loop do
|
2533
|
+
if current_token && current_token[:type] == :identifier
|
2534
|
+
param_name = current_token[:value]
|
2535
|
+
param_token = current_token
|
2536
|
+
advance
|
2537
|
+
|
2538
|
+
# Check for type annotation
|
2539
|
+
param_type = nil
|
2540
|
+
if current_token && current_token[:type] == :colon
|
2541
|
+
advance # Skip ':'
|
2542
|
+
param_type = parse_type
|
2543
|
+
end
|
2544
|
+
|
2545
|
+
params << {
|
2546
|
+
type: :parameter,
|
2547
|
+
name: param_name,
|
2548
|
+
param_type: param_type,
|
2549
|
+
line: param_token[:line],
|
2550
|
+
column: param_token[:column]
|
2551
|
+
}
|
2552
|
+
else
|
2553
|
+
@errors << {
|
2554
|
+
message: "Expected parameter name in lambda expression",
|
2555
|
+
position: @position,
|
2556
|
+
line: current_token ? current_token[:line] : 0,
|
2557
|
+
column: current_token ? current_token[:column] : 0
|
2558
|
+
}
|
2559
|
+
break
|
2560
|
+
end
|
2561
|
+
|
2562
|
+
if current_token && current_token[:type] == :comma
|
2563
|
+
advance # Skip comma
|
2564
|
+
else
|
2565
|
+
break
|
2566
|
+
end
|
2567
|
+
end
|
2568
|
+
end
|
2569
|
+
|
2570
|
+
expect(:paren_close) # Skip ')'
|
2571
|
+
|
2572
|
+
# Check for return type annotation
|
2573
|
+
return_type = nil
|
2574
|
+
if current_token && current_token[:type] == :colon
|
2575
|
+
advance # Skip ':'
|
2576
|
+
return_type = parse_type
|
2577
|
+
end
|
2578
|
+
|
2579
|
+
# Expect the arrow '=>'
|
2580
|
+
arrow_found = false
|
2581
|
+
if current_token && current_token[:type] == :operator && current_token[:value] == '=>'
|
2582
|
+
arrow_found = true
|
2583
|
+
advance # Skip '=>'
|
2584
|
+
else
|
2585
|
+
@errors << {
|
2586
|
+
message: "Expected '=>' in lambda expression",
|
2587
|
+
position: @position,
|
2588
|
+
line: current_token ? current_token[:line] : 0,
|
2589
|
+
column: current_token ? current_token[:column] : 0
|
2590
|
+
}
|
2591
|
+
end
|
2592
|
+
|
2593
|
+
# Parse the lambda body - check if it's a block or an expression
|
2594
|
+
if arrow_found
|
2595
|
+
if current_token && current_token[:type] == :brace_open
|
2596
|
+
# Parse block body
|
2597
|
+
advance # Skip '{'
|
2598
|
+
|
2599
|
+
block_body = []
|
2600
|
+
brace_count = 1 # Track nested braces
|
2601
|
+
|
2602
|
+
while @position < @tokens.length && brace_count > 0
|
2603
|
+
if current_token[:type] == :brace_open
|
2604
|
+
brace_count += 1
|
2605
|
+
elsif current_token[:type] == :brace_close
|
2606
|
+
brace_count -= 1
|
2607
|
+
|
2608
|
+
# Skip the final closing brace, but don't process it as a statement
|
2609
|
+
if brace_count == 0
|
2610
|
+
advance
|
2611
|
+
break
|
2612
|
+
end
|
2613
|
+
end
|
2614
|
+
|
2615
|
+
stmt = parse_statement
|
2616
|
+
block_body << stmt if stmt
|
2617
|
+
|
2618
|
+
# Break if we've reached the end or found the matching closing brace
|
2619
|
+
if brace_count == 0 || @position >= @tokens.length
|
2620
|
+
break
|
2621
|
+
end
|
2622
|
+
end
|
2623
|
+
|
2624
|
+
body = {
|
2625
|
+
type: :block_expression,
|
2626
|
+
statements: block_body,
|
2627
|
+
line: line,
|
2628
|
+
column: column
|
2629
|
+
}
|
2630
|
+
else
|
2631
|
+
# Simple expression body
|
2632
|
+
body = parse_expression
|
2633
|
+
end
|
2634
|
+
else
|
2635
|
+
body = nil
|
2636
|
+
end
|
2637
|
+
|
2638
|
+
return {
|
2639
|
+
type: :lambda_expression,
|
2640
|
+
params: params,
|
2641
|
+
return_type: return_type,
|
2642
|
+
body: body,
|
2643
|
+
line: line,
|
2644
|
+
column: column
|
2645
|
+
}
|
2646
|
+
end
|
2647
|
+
|
2648
|
+
def parse_dict_literal(line, column)
|
2649
|
+
advance # Skip '{'
|
2650
|
+
entries = []
|
2651
|
+
|
2652
|
+
# Handle whitespace after opening brace
|
2653
|
+
while current_token && current_token[:type] == :whitespace
|
2654
|
+
advance
|
2655
|
+
end
|
2656
|
+
|
2657
|
+
# Empty dictionary
|
2658
|
+
if current_token && current_token[:type] == :brace_close
|
2659
|
+
advance # Skip '}'
|
2660
|
+
return {
|
2661
|
+
type: :dict_literal,
|
2662
|
+
entries: entries,
|
2663
|
+
line: line,
|
2664
|
+
column: column
|
2665
|
+
}
|
2666
|
+
end
|
2667
|
+
|
2668
|
+
# Parse dictionary entries
|
2669
|
+
while current_token && current_token[:type] != :brace_close
|
2670
|
+
# Skip any backslash line continuation markers and whitespace
|
2671
|
+
while current_token && (current_token[:type] == :backslash || current_token[:type] == :whitespace || current_token[:type] == :line_continuation)
|
2672
|
+
advance
|
2673
|
+
end
|
2674
|
+
|
2675
|
+
# Break if we reached the end or found closing brace
|
2676
|
+
if !current_token || current_token[:type] == :brace_close
|
2677
|
+
break
|
2678
|
+
end
|
2679
|
+
|
2680
|
+
# Parse key (string or identifier)
|
2681
|
+
key = nil
|
2682
|
+
if current_token && (current_token[:type] == :string || current_token[:type] == :identifier)
|
2683
|
+
key = current_token[:value]
|
2684
|
+
advance # Skip key
|
2685
|
+
else
|
2686
|
+
@errors << {
|
2687
|
+
message: "Expected string or identifier as dictionary key",
|
2688
|
+
position: @position,
|
2689
|
+
line: current_token ? current_token[:line] : 0,
|
2690
|
+
column: current_token ? current_token[:column] : 0
|
2691
|
+
}
|
2692
|
+
# Try to recover by advancing until we find a colon or closing brace
|
2693
|
+
while current_token && current_token[:type] != :colon && current_token[:type] != :brace_close
|
2694
|
+
advance
|
2695
|
+
end
|
2696
|
+
if !current_token || current_token[:type] == :brace_close
|
2697
|
+
break
|
2698
|
+
end
|
2699
|
+
end
|
2700
|
+
|
2701
|
+
# Skip whitespace after key
|
2702
|
+
while current_token && current_token[:type] == :whitespace
|
2703
|
+
advance
|
2704
|
+
end
|
2705
|
+
|
2706
|
+
# Expect colon
|
2707
|
+
if current_token && current_token[:type] == :colon
|
2708
|
+
advance # Skip colon
|
2709
|
+
else
|
2710
|
+
@errors << {
|
2711
|
+
message: "Expected colon after dictionary key",
|
2712
|
+
position: @position,
|
2713
|
+
line: current_token ? current_token[:line] : 0,
|
2714
|
+
column: current_token ? current_token[:column] : 0
|
2715
|
+
}
|
2716
|
+
end
|
2717
|
+
|
2718
|
+
# Skip whitespace after colon
|
2719
|
+
while current_token && current_token[:type] == :whitespace
|
2720
|
+
advance
|
2721
|
+
end
|
2722
|
+
|
2723
|
+
# Parse value
|
2724
|
+
if current_token && current_token[:type] == :brace_open
|
2725
|
+
lambda_or_dict = parse_vim_lambda_or_dict(line, column)
|
2726
|
+
value = lambda_or_dict
|
2727
|
+
else
|
2728
|
+
value = parse_expression
|
2729
|
+
end
|
2730
|
+
|
2731
|
+
entries << {
|
2732
|
+
key: key,
|
2733
|
+
value: value
|
2734
|
+
}
|
2735
|
+
|
2736
|
+
# After parsing an entry, skip whitespace and look for comma or closing brace
|
2737
|
+
while current_token && current_token[:type] == :whitespace
|
2738
|
+
advance
|
2739
|
+
end
|
2740
|
+
|
2741
|
+
# Check for comma (indicates more entries follow)
|
2742
|
+
if current_token && current_token[:type] == :comma
|
2743
|
+
advance # Skip comma
|
2744
|
+
# Skip any line continuations and whitespace after comma
|
2745
|
+
while current_token && (current_token[:type] == :whitespace ||
|
2746
|
+
current_token[:type] == :backslash ||
|
2747
|
+
current_token[:type] == :line_continuation)
|
2748
|
+
advance
|
2749
|
+
end
|
2750
|
+
else
|
2751
|
+
# No comma - skip any line continuations before checking for closing brace
|
2752
|
+
while current_token && (current_token[:type] == :backslash ||
|
2753
|
+
current_token[:type] == :line_continuation ||
|
2754
|
+
current_token[:type] == :whitespace)
|
2755
|
+
advance
|
2756
|
+
end
|
2757
|
+
|
2758
|
+
# After skipping continuations, we should find the closing brace
|
2759
|
+
if current_token && current_token[:type] == :brace_close
|
2760
|
+
# This is fine - last entry without trailing comma
|
2761
|
+
break
|
2762
|
+
elsif current_token && current_token[:type] != :brace_close
|
2763
|
+
@errors << {
|
2764
|
+
message: "Expected comma or closing brace after dictionary entry",
|
2765
|
+
position: @position,
|
2766
|
+
line: current_token ? current_token[:line] : 0,
|
2767
|
+
column: current_token ? current_token[:column] : 0
|
2768
|
+
}
|
2769
|
+
break
|
2770
|
+
end
|
2771
|
+
end
|
2772
|
+
end
|
2773
|
+
|
2774
|
+
# Make sure we have a closing brace
|
2775
|
+
if current_token && current_token[:type] == :brace_close
|
2776
|
+
advance # Skip '}'
|
2777
|
+
else
|
2778
|
+
@errors << {
|
2779
|
+
message: "Expected closing brace for dictionary",
|
2780
|
+
position: @position,
|
2781
|
+
line: current_token ? current_token[:line] : 0,
|
2782
|
+
column: current_token ? current_token[:column] : 0
|
2783
|
+
}
|
2784
|
+
end
|
2785
|
+
|
2786
|
+
{
|
2787
|
+
type: :dict_literal,
|
2788
|
+
entries: entries,
|
2789
|
+
line: line,
|
2790
|
+
column: column
|
2791
|
+
}
|
2792
|
+
end
|
2793
|
+
|
2794
|
+
def parse_list_literal(line, column)
|
2795
|
+
advance # Skip '['
|
2796
|
+
elements = []
|
2797
|
+
|
2798
|
+
# Empty list
|
2799
|
+
if current_token && current_token[:type] == :bracket_close
|
2800
|
+
advance # Skip ']'
|
2801
|
+
return {
|
2802
|
+
type: :list_literal,
|
2803
|
+
elements: elements,
|
2804
|
+
line: line,
|
2805
|
+
column: column
|
2806
|
+
}
|
2807
|
+
end
|
2808
|
+
|
2809
|
+
# Parse list elements
|
2810
|
+
while @position < @tokens.length
|
2811
|
+
# Handle line continuation characters (backslash)
|
2812
|
+
if current_token && [:backslash, :line_continuation].include?(current_token[:type])
|
2813
|
+
# Skip the backslash token
|
2814
|
+
advance
|
2815
|
+
|
2816
|
+
# Skip any whitespace that might follow the backslash
|
2817
|
+
while current_token && current_token[:type] == :whitespace
|
2818
|
+
advance
|
2819
|
+
end
|
2820
|
+
|
2821
|
+
# Don't add a comma - just continue to parse the next element
|
2822
|
+
next
|
2823
|
+
end
|
2824
|
+
|
2825
|
+
# Check if we've reached the end of the list
|
2826
|
+
if current_token && current_token[:type] == :bracket_close
|
2827
|
+
advance # Skip ']'
|
2828
|
+
break
|
2829
|
+
end
|
2830
|
+
|
2831
|
+
element = parse_expression
|
2832
|
+
elements << element if element
|
2833
|
+
|
2834
|
+
# Continue if there's a comma
|
2835
|
+
if current_token && current_token[:type] == :comma
|
2836
|
+
advance # Skip comma
|
2837
|
+
# Or if the next token is a backslash (line continuation)
|
2838
|
+
else
|
2839
|
+
# binding.pry
|
2840
|
+
# If no comma and not a closing bracket or backslash, then it's an error
|
2841
|
+
# if current_token && current_token[:type] != :bracket_close
|
2842
|
+
# @errors << {
|
2843
|
+
# message: "Expected comma, backslash, or closing bracket after list element",
|
2844
|
+
# position: @position,
|
2845
|
+
# line: current_token[:line],
|
2846
|
+
# column: current_token[:column]
|
2847
|
+
# }
|
2848
|
+
# end
|
2849
|
+
|
2850
|
+
# We still want to skip the closing bracket if it's there
|
2851
|
+
if current_token && current_token[:type] == :bracket_close
|
2852
|
+
advance
|
2853
|
+
break
|
2854
|
+
end
|
2855
|
+
advance
|
2856
|
+
#next
|
2857
|
+
end
|
2858
|
+
end
|
2859
|
+
|
2860
|
+
# Check if we have a second list immediately following this one (Vim's special syntax)
|
2861
|
+
if current_token && current_token[:type] == :bracket_open
|
2862
|
+
next_list = parse_list_literal(current_token[:line], current_token[:column])
|
2863
|
+
|
2864
|
+
return {
|
2865
|
+
type: :list_concat_expression,
|
2866
|
+
left: {
|
2867
|
+
type: :list_literal,
|
2868
|
+
elements: elements,
|
2869
|
+
line: line,
|
2870
|
+
column: column
|
2871
|
+
},
|
2872
|
+
right: next_list,
|
2873
|
+
line: line,
|
2874
|
+
column: column
|
2875
|
+
}
|
2876
|
+
end
|
2877
|
+
|
2878
|
+
return {
|
2879
|
+
type: :list_literal,
|
2880
|
+
elements: elements,
|
2881
|
+
line: line,
|
2882
|
+
column: column
|
2883
|
+
}
|
2884
|
+
end
|
2885
|
+
|
2886
|
+
def parse_unary_expression
|
2887
|
+
# Check for unary operators (!, -, +)
|
2888
|
+
if current_token && current_token[:type] == :operator &&
|
2889
|
+
['!', '-', '+'].include?(current_token[:value])
|
2890
|
+
|
2891
|
+
op_token = advance # Skip the operator
|
2892
|
+
operand = parse_unary_expression # Recursively parse the operand
|
2893
|
+
|
2894
|
+
return {
|
2895
|
+
type: :unary_expression,
|
2896
|
+
operator: op_token[:value],
|
2897
|
+
operand: operand,
|
2898
|
+
line: op_token[:line],
|
2899
|
+
column: op_token[:column]
|
2900
|
+
}
|
2901
|
+
end
|
2902
|
+
|
2903
|
+
# If no unary operator, parse a primary expression
|
2904
|
+
return parse_primary_expression
|
2905
|
+
end
|
2906
|
+
|
2907
|
+
def parse_function_call(name, line, column)
|
2908
|
+
expect(:paren_open)
|
2909
|
+
|
2910
|
+
args = []
|
2911
|
+
|
2912
|
+
# Special handling for Vim functions that take code strings as arguments
|
2913
|
+
special_functions = ['map', 'reduce', 'sort', 'call', 'eval', 'execute', 'exec']
|
2914
|
+
is_special_function = special_functions.include?(name)
|
2915
|
+
|
2916
|
+
# Parse arguments until we find a closing parenthesis
|
2917
|
+
while @position < @tokens.length && current_token && current_token[:type] != :paren_close
|
2918
|
+
# Skip comments inside parameter lists
|
2919
|
+
if current_token && current_token[:type] == :comment
|
2920
|
+
advance
|
2921
|
+
next
|
2922
|
+
end
|
2923
|
+
if is_special_function && current_token && current_token[:type] == :string
|
2924
|
+
# For functions like map(), filter(), directly add the string as an argument
|
2925
|
+
string_token = parse_string
|
2926
|
+
args << {
|
2927
|
+
type: :literal,
|
2928
|
+
value: string_token[:value],
|
2929
|
+
token_type: :string,
|
2930
|
+
line: string_token[:line],
|
2931
|
+
column: string_token[:column]
|
2932
|
+
}
|
2933
|
+
else
|
2934
|
+
# Parse the argument
|
2935
|
+
arg = parse_expression
|
2936
|
+
args << arg if arg
|
2937
|
+
end
|
2938
|
+
|
2939
|
+
# Break if we hit the closing paren
|
2940
|
+
if current_token && current_token[:type] == :paren_close
|
2941
|
+
break
|
2942
|
+
end
|
2943
|
+
# If we have a comma, advance past it and continue
|
2944
|
+
if current_token && current_token[:type] == :comma
|
2945
|
+
advance
|
2946
|
+
# If we don't have a comma and we're not at the end, it's an error
|
2947
|
+
elsif current_token && current_token[:type] != :paren_close && current_token[:type] != :comment
|
2948
|
+
@errors << {
|
2949
|
+
message: "Expected comma or closing parenthesis after argument",
|
2950
|
+
position: @position,
|
2951
|
+
line: current_token[:line],
|
2952
|
+
column: current_token[:column]
|
2953
|
+
}
|
2954
|
+
break
|
2955
|
+
end
|
2956
|
+
end
|
2957
|
+
|
2958
|
+
expect(:paren_close)
|
2959
|
+
|
2960
|
+
{
|
2961
|
+
type: :function_call,
|
2962
|
+
name: name,
|
2963
|
+
arguments: args,
|
2964
|
+
line: line,
|
2965
|
+
column: column
|
2966
|
+
}
|
2967
|
+
end
|
2968
|
+
|
2969
|
+
def parse_vim_lambda_or_dict(line, column)
|
2970
|
+
# Save current position to peek ahead
|
2971
|
+
start_position = @position
|
2972
|
+
|
2973
|
+
advance # Skip opening brace
|
2974
|
+
|
2975
|
+
# Check if this is a lambda by looking for parameter names followed by arrow
|
2976
|
+
is_lambda = false
|
2977
|
+
param_names = []
|
2978
|
+
|
2979
|
+
# Check for immediate arrow (no parameters lambda)
|
2980
|
+
if current_token && current_token[:type] == :operator && current_token[:value] == '->'
|
2981
|
+
is_lambda = true
|
2982
|
+
else
|
2983
|
+
# Parse until closing brace or arrow
|
2984
|
+
while @position < @tokens.length && current_token[:type] != :brace_close
|
2985
|
+
if current_token[:type] == :identifier
|
2986
|
+
param_names << current_token[:value]
|
2987
|
+
advance
|
2988
|
+
|
2989
|
+
# Skip comma between parameters
|
2990
|
+
if current_token && current_token[:type] == :comma
|
2991
|
+
advance
|
2992
|
+
next
|
2993
|
+
end
|
2994
|
+
elsif current_token[:type] == :operator && current_token[:value] == '->'
|
2995
|
+
is_lambda = true
|
2996
|
+
break
|
2997
|
+
else
|
2998
|
+
# If we see a colon, this is likely a nested dictionary
|
2999
|
+
if current_token && current_token[:type] == :colon
|
3000
|
+
break
|
3001
|
+
end
|
3002
|
+
advance
|
3003
|
+
end
|
3004
|
+
end
|
3005
|
+
end
|
3006
|
+
|
3007
|
+
# Reset position
|
3008
|
+
@position = start_position
|
3009
|
+
|
3010
|
+
if is_lambda
|
3011
|
+
return parse_vim_lambda(line, column)
|
3012
|
+
else
|
3013
|
+
return parse_dict_literal(line, column)
|
3014
|
+
end
|
3015
|
+
end
|
3016
|
+
|
3017
|
+
def parse_import_statement
|
3018
|
+
token = advance # Skip 'import'
|
3019
|
+
line = token[:line]
|
3020
|
+
column = token[:column]
|
3021
|
+
|
3022
|
+
# Handle 'import autoload'
|
3023
|
+
is_autoload = false
|
3024
|
+
module_name = nil
|
3025
|
+
path = nil
|
3026
|
+
|
3027
|
+
if current_token && current_token[:type] == :identifier && current_token[:value] == 'autoload'
|
3028
|
+
is_autoload = true
|
3029
|
+
module_name = advance[:value] # Store "autoload" as the module name
|
3030
|
+
|
3031
|
+
# After "autoload" keyword, expect a string path
|
3032
|
+
if current_token && current_token[:type] == :string
|
3033
|
+
path = current_token[:value]
|
3034
|
+
advance
|
3035
|
+
else
|
3036
|
+
@errors << {
|
3037
|
+
message: "Expected string path after 'autoload'",
|
3038
|
+
position: @position,
|
3039
|
+
line: current_token ? current_token[:line] : 0,
|
3040
|
+
column: current_token ? current_token[:column] : 0
|
3041
|
+
}
|
3042
|
+
end
|
3043
|
+
else
|
3044
|
+
# Regular import with a string path
|
3045
|
+
if current_token && current_token[:type] == :string
|
3046
|
+
path = current_token[:value]
|
3047
|
+
|
3048
|
+
# Extract module name from the path
|
3049
|
+
# This is simplified logic - you might need more complex extraction
|
3050
|
+
module_name = path.gsub(/['"]/, '').split('/').last.split('.').first
|
3051
|
+
|
3052
|
+
advance
|
3053
|
+
else
|
3054
|
+
# Handle other import formats
|
3055
|
+
module_expr = parse_expression()
|
3056
|
+
if module_expr && module_expr[:type] == :literal && module_expr[:token_type] == :string
|
3057
|
+
path = module_expr[:value]
|
3058
|
+
module_name = path.gsub(/['"]/, '').split('/').last.split('.').first
|
3059
|
+
end
|
3060
|
+
end
|
3061
|
+
end
|
3062
|
+
|
3063
|
+
# Handle 'as name'
|
3064
|
+
as_name = nil
|
3065
|
+
if current_token && current_token[:type] == :identifier && current_token[:value] == 'as'
|
3066
|
+
advance # Skip 'as'
|
3067
|
+
if current_token && current_token[:type] == :identifier
|
3068
|
+
as_name = advance[:value]
|
3069
|
+
else
|
3070
|
+
@errors << {
|
3071
|
+
message: "Expected identifier after 'as'",
|
3072
|
+
position: @position,
|
3073
|
+
line: current_token ? current_token[:line] : 0,
|
3074
|
+
column: current_token ? current_token[:column] : 0
|
3075
|
+
}
|
3076
|
+
end
|
3077
|
+
end
|
3078
|
+
|
3079
|
+
{
|
3080
|
+
type: :import_statement,
|
3081
|
+
module: module_name,
|
3082
|
+
path: path,
|
3083
|
+
is_autoload: is_autoload,
|
3084
|
+
as_name: as_name,
|
3085
|
+
line: line,
|
3086
|
+
column: column
|
3087
|
+
}
|
3088
|
+
end
|
3089
|
+
def parse_export_statement
|
3090
|
+
token = advance # Skip 'export'
|
3091
|
+
line = token[:line]
|
3092
|
+
column = token[:column]
|
3093
|
+
|
3094
|
+
# Export can be followed by var/const/def/function declarations
|
3095
|
+
if !current_token
|
3096
|
+
@errors << {
|
3097
|
+
message: "Expected declaration after export",
|
3098
|
+
position: @position,
|
3099
|
+
line: line,
|
3100
|
+
column: column
|
3101
|
+
}
|
3102
|
+
return nil
|
3103
|
+
end
|
3104
|
+
|
3105
|
+
exported_item = nil
|
3106
|
+
|
3107
|
+
if current_token[:type] == :keyword
|
3108
|
+
case current_token[:value]
|
3109
|
+
when 'def'
|
3110
|
+
exported_item = parse_def_function
|
3111
|
+
when 'function'
|
3112
|
+
exported_item = parse_legacy_function
|
3113
|
+
when 'var', 'const', 'final'
|
3114
|
+
exported_item = parse_variable_declaration
|
3115
|
+
when 'class'
|
3116
|
+
# Handle class export when implemented
|
3117
|
+
@errors << {
|
3118
|
+
message: "Class export not implemented yet",
|
3119
|
+
position: @position,
|
3120
|
+
line: current_token[:line],
|
3121
|
+
column: current_token[:column]
|
3122
|
+
}
|
3123
|
+
advance
|
3124
|
+
return nil
|
3125
|
+
else
|
3126
|
+
@errors << {
|
3127
|
+
message: "Unexpected keyword after export: #{current_token[:value]}",
|
3128
|
+
position: @position,
|
3129
|
+
line: current_token[:line],
|
3130
|
+
column: current_token[:column]
|
3131
|
+
}
|
3132
|
+
advance
|
3133
|
+
return nil
|
3134
|
+
end
|
3135
|
+
else
|
3136
|
+
@errors << {
|
3137
|
+
message: "Expected declaration after export",
|
3138
|
+
position: @position,
|
3139
|
+
line: current_token[:line],
|
3140
|
+
column: current_token[:column]
|
3141
|
+
}
|
3142
|
+
advance
|
3143
|
+
return nil
|
3144
|
+
end
|
3145
|
+
|
3146
|
+
{
|
3147
|
+
type: :export_statement,
|
3148
|
+
export: exported_item,
|
3149
|
+
line: line,
|
3150
|
+
column: column
|
3151
|
+
}
|
3152
|
+
end
|
3153
|
+
|
3154
|
+
def parse_command_definition
|
3155
|
+
token = advance # Skip 'command' or 'command!'
|
3156
|
+
line = token[:line]
|
3157
|
+
column = token[:column]
|
3158
|
+
|
3159
|
+
# Check if the command has a bang (!)
|
3160
|
+
has_bang = false
|
3161
|
+
if current_token && current_token[:type] == :operator && current_token[:value] == '!'
|
3162
|
+
has_bang = true
|
3163
|
+
advance # Skip '!'
|
3164
|
+
end
|
3165
|
+
|
3166
|
+
# Parse command options/attributes (starting with hyphen)
|
3167
|
+
attributes = []
|
3168
|
+
while current_token && (
|
3169
|
+
(current_token[:type] == :operator && current_token[:value] == '-') ||
|
3170
|
+
(current_token[:type] == :identifier && current_token[:value].start_with?('-'))
|
3171
|
+
)
|
3172
|
+
# Handle option as a single attribute if it's already combined
|
3173
|
+
if current_token[:type] == :identifier && current_token[:value].start_with?('-')
|
3174
|
+
attributes << current_token[:value]
|
3175
|
+
advance
|
3176
|
+
else
|
3177
|
+
# Otherwise combine the hyphen with the following identifier
|
3178
|
+
advance # Skip the hyphen
|
3179
|
+
if current_token && current_token[:type] == :identifier
|
3180
|
+
attributes << "-#{current_token[:value]}"
|
3181
|
+
advance
|
3182
|
+
end
|
3183
|
+
end
|
3184
|
+
|
3185
|
+
# If there's an = followed by a value, include it in the attribute
|
3186
|
+
if current_token && current_token[:type] == :operator && current_token[:value] == '='
|
3187
|
+
attribute = attributes.pop # Take the last attribute we added
|
3188
|
+
advance # Skip the '='
|
3189
|
+
|
3190
|
+
# Get the value (number or identifier)
|
3191
|
+
if current_token && (current_token[:type] == :number || current_token[:type] == :identifier)
|
3192
|
+
attribute += "=#{current_token[:value]}"
|
3193
|
+
attributes << attribute
|
3194
|
+
advance
|
3195
|
+
end
|
3196
|
+
end
|
3197
|
+
end
|
3198
|
+
|
3199
|
+
# Parse the command name
|
3200
|
+
command_name = nil
|
3201
|
+
if current_token && current_token[:type] == :identifier
|
3202
|
+
command_name = current_token[:value]
|
3203
|
+
advance
|
3204
|
+
else
|
3205
|
+
@errors << {
|
3206
|
+
message: "Expected command name",
|
3207
|
+
position: @position,
|
3208
|
+
line: current_token ? current_token[:line] : 0,
|
3209
|
+
column: current_token ? current_token[:column] : 0
|
3210
|
+
}
|
3211
|
+
end
|
3212
|
+
|
3213
|
+
# Parse the command implementation - collect all remaining tokens as raw parts
|
3214
|
+
implementation_parts = []
|
3215
|
+
while @position < @tokens.length
|
3216
|
+
# Break on end of line or comment
|
3217
|
+
if !current_token || current_token[:type] == :comment ||
|
3218
|
+
(current_token[:value] == "\n" && !current_token[:value].start_with?("\\"))
|
3219
|
+
break
|
3220
|
+
end
|
3221
|
+
|
3222
|
+
implementation_parts << current_token
|
3223
|
+
advance
|
3224
|
+
end
|
3225
|
+
|
3226
|
+
{
|
3227
|
+
type: :command_definition,
|
3228
|
+
name: command_name,
|
3229
|
+
has_bang: has_bang,
|
3230
|
+
attributes: attributes,
|
3231
|
+
implementation: implementation_parts,
|
3232
|
+
line: line,
|
3233
|
+
column: column
|
3234
|
+
}
|
3235
|
+
end
|
3236
|
+
|
3237
|
+
def parse_legacy_function
|
3238
|
+
token = advance # Skip 'function'
|
3239
|
+
line = token[:line]
|
3240
|
+
column = token[:column]
|
3241
|
+
|
3242
|
+
# Check for bang (!) in function definition
|
3243
|
+
has_bang = false
|
3244
|
+
if current_token && current_token[:type] == :operator && current_token[:value] == '!'
|
3245
|
+
has_bang = true
|
3246
|
+
advance # Skip '!'
|
3247
|
+
end
|
3248
|
+
|
3249
|
+
# For script-local functions or other scoped functions
|
3250
|
+
is_script_local = false
|
3251
|
+
function_scope = nil
|
3252
|
+
|
3253
|
+
# Check if we have a script-local function (s:)
|
3254
|
+
if current_token && current_token[:type] == :script_local
|
3255
|
+
is_script_local = true
|
3256
|
+
function_scope = current_token[:value]
|
3257
|
+
advance # Skip s: prefix
|
3258
|
+
elsif current_token && current_token[:type] == :identifier && current_token[:value].include?(':')
|
3259
|
+
# Handle other scoped functions like g: or b:
|
3260
|
+
parts = current_token[:value].split(':')
|
3261
|
+
if parts.length == 2
|
3262
|
+
function_scope = parts[0] + ':'
|
3263
|
+
advance
|
3264
|
+
end
|
3265
|
+
end
|
3266
|
+
|
3267
|
+
# Now handle the function name, which might be separate from the scope
|
3268
|
+
name = nil
|
3269
|
+
if !is_script_local && function_scope.nil? && current_token && current_token[:type] == :identifier
|
3270
|
+
name = current_token
|
3271
|
+
advance
|
3272
|
+
elsif is_script_local || !function_scope.nil?
|
3273
|
+
if current_token && current_token[:type] == :identifier
|
3274
|
+
name = current_token
|
3275
|
+
advance
|
3276
|
+
end
|
3277
|
+
else
|
3278
|
+
name = expect(:identifier)
|
3279
|
+
end
|
3280
|
+
|
3281
|
+
# Parse parameter list
|
3282
|
+
expect(:paren_open)
|
3283
|
+
params = parse_parameter_list_legacy
|
3284
|
+
expect(:paren_close)
|
3285
|
+
|
3286
|
+
# Check for optional attributes (range, dict, abort, closure)
|
3287
|
+
attributes = []
|
3288
|
+
while current_token
|
3289
|
+
if current_token[:type] == :keyword && current_token[:value] == 'abort'
|
3290
|
+
attributes << advance[:value]
|
3291
|
+
elsif current_token[:type] == :identifier &&
|
3292
|
+
['range', 'dict', 'closure'].include?(current_token[:value])
|
3293
|
+
attributes << advance[:value]
|
3294
|
+
else
|
3295
|
+
break
|
3296
|
+
end
|
3297
|
+
end
|
3298
|
+
|
3299
|
+
# Parse function body
|
3300
|
+
body = []
|
3301
|
+
while @position < @tokens.length
|
3302
|
+
if current_token && current_token[:type] == :keyword &&
|
3303
|
+
['endfunction', 'endfunc'].include?(current_token[:value])
|
3304
|
+
break
|
3305
|
+
end
|
3306
|
+
|
3307
|
+
stmt = parse_statement
|
3308
|
+
body << stmt if stmt
|
3309
|
+
end
|
606
3310
|
|
607
|
-
|
608
|
-
|
3311
|
+
# Expect endfunction/endfunc
|
3312
|
+
if current_token && current_token[:type] == :keyword &&
|
3313
|
+
['endfunction', 'endfunc'].include?(current_token[:value])
|
3314
|
+
advance # Skip 'endfunction' or 'endfunc'
|
3315
|
+
else
|
3316
|
+
# Only add an error if we haven't reached the end of the file
|
3317
|
+
if @position < @tokens.length
|
3318
|
+
@errors << {
|
3319
|
+
message: "Expected 'endfunction' or 'endfunc'",
|
3320
|
+
position: @position,
|
3321
|
+
line: current_token ? current_token[:line] : 0,
|
3322
|
+
column: current_token ? current_token[:column] : 0
|
3323
|
+
}
|
3324
|
+
end
|
3325
|
+
end
|
609
3326
|
|
610
|
-
|
3327
|
+
function_name = name ? name[:value] : nil
|
3328
|
+
if function_scope
|
3329
|
+
function_name = function_scope + function_name if function_name
|
3330
|
+
end
|
3331
|
+
|
3332
|
+
{
|
3333
|
+
type: :legacy_function,
|
3334
|
+
name: function_name,
|
3335
|
+
is_script_local: is_script_local,
|
3336
|
+
scope: function_scope,
|
3337
|
+
params: params,
|
3338
|
+
has_bang: has_bang,
|
3339
|
+
attributes: attributes,
|
3340
|
+
body: body,
|
3341
|
+
line: line,
|
3342
|
+
column: column
|
3343
|
+
}
|
3344
|
+
end
|
3345
|
+
|
3346
|
+
# Legacy function parameters are different - they can use a:name syntax
|
3347
|
+
def parse_parameter_list_legacy
|
3348
|
+
params = []
|
3349
|
+
|
3350
|
+
# Empty parameter list
|
3351
|
+
if current_token && current_token[:type] == :paren_close
|
3352
|
+
return params
|
3353
|
+
end
|
3354
|
+
|
3355
|
+
# Handle special case: varargs at the start of the parameter list
|
3356
|
+
if current_token && (
|
3357
|
+
(current_token[:type] == :ellipsis) ||
|
3358
|
+
(current_token[:type] == :operator && current_token[:value] == '.')
|
3359
|
+
)
|
3360
|
+
if current_token[:type] == :ellipsis
|
3361
|
+
token = advance
|
3362
|
+
params << {
|
3363
|
+
type: :var_args_legacy,
|
3364
|
+
name: '...',
|
3365
|
+
line: token[:line],
|
3366
|
+
column: token[:column]
|
3367
|
+
}
|
3368
|
+
return params
|
3369
|
+
else
|
3370
|
+
# Count consecutive dots
|
3371
|
+
dot_count = 0
|
3372
|
+
first_dot_token = current_token
|
3373
|
+
|
3374
|
+
# Store the line and column before we advance
|
3375
|
+
dot_line = first_dot_token[:line]
|
3376
|
+
dot_column = first_dot_token[:column]
|
3377
|
+
|
3378
|
+
while current_token && current_token[:type] == :operator && current_token[:value] == '.'
|
3379
|
+
dot_count += 1
|
3380
|
+
advance
|
3381
|
+
end
|
3382
|
+
|
3383
|
+
if dot_count == 3
|
3384
|
+
params << {
|
3385
|
+
type: :var_args_legacy,
|
3386
|
+
name: '...',
|
3387
|
+
line: dot_line,
|
3388
|
+
column: dot_column
|
3389
|
+
}
|
3390
|
+
return params
|
3391
|
+
end
|
3392
|
+
end
|
3393
|
+
end
|
3394
|
+
|
3395
|
+
# Regular parameter parsing (existing logic)
|
3396
|
+
loop do
|
3397
|
+
if current_token && current_token[:type] == :identifier
|
3398
|
+
param_name = advance
|
3399
|
+
|
3400
|
+
# Check for default value
|
3401
|
+
default_value = nil
|
3402
|
+
if current_token && current_token[:type] == :operator && current_token[:value] == '='
|
3403
|
+
advance # Skip '='
|
3404
|
+
default_value = parse_expression
|
3405
|
+
end
|
3406
|
+
|
3407
|
+
params << {
|
3408
|
+
type: :parameter,
|
3409
|
+
name: param_name[:value],
|
3410
|
+
default_value: default_value,
|
3411
|
+
optional: default_value != nil,
|
3412
|
+
line: param_name[:line],
|
3413
|
+
column: param_name[:column]
|
3414
|
+
}
|
3415
|
+
elsif current_token && current_token[:type] == :arg_variable
|
3416
|
+
param_name = advance
|
3417
|
+
name_without_prefix = param_name[:value].sub(/^a:/, '')
|
3418
|
+
|
3419
|
+
# Check for default value
|
3420
|
+
default_value = nil
|
3421
|
+
if current_token && current_token[:type] == :operator && current_token[:value] == '='
|
3422
|
+
advance # Skip '='
|
3423
|
+
default_value = parse_expression
|
3424
|
+
end
|
3425
|
+
|
3426
|
+
params << {
|
3427
|
+
type: :parameter,
|
3428
|
+
name: name_without_prefix,
|
3429
|
+
default_value: default_value,
|
3430
|
+
optional: default_value != nil,
|
3431
|
+
line: param_name[:line],
|
3432
|
+
column: param_name[:column],
|
3433
|
+
is_arg_prefixed: true
|
3434
|
+
}
|
3435
|
+
else
|
3436
|
+
# We might be at varargs after other parameters
|
3437
|
+
if current_token && (
|
3438
|
+
(current_token[:type] == :ellipsis) ||
|
3439
|
+
(current_token[:type] == :operator && current_token[:value] == '.')
|
3440
|
+
)
|
3441
|
+
# Add debug
|
3442
|
+
#puts "Found potential varargs token: #{current_token[:type]} #{current_token[:value]}"
|
3443
|
+
|
3444
|
+
if current_token[:type] == :ellipsis
|
3445
|
+
token = current_token # STORE the token before advancing
|
3446
|
+
advance
|
3447
|
+
params << {
|
3448
|
+
type: :var_args_legacy,
|
3449
|
+
name: '...',
|
3450
|
+
line: token[:line], # Use stored token
|
3451
|
+
column: token[:column]
|
3452
|
+
}
|
3453
|
+
else
|
3454
|
+
dot_count = 0
|
3455
|
+
first_dot_token = current_token
|
3456
|
+
|
3457
|
+
# Store line/column BEFORE advancing
|
3458
|
+
dot_line = first_dot_token[:line]
|
3459
|
+
dot_column = first_dot_token[:column]
|
3460
|
+
|
3461
|
+
#puts "Starting dot sequence at line #{dot_line}, column #{dot_column}"
|
3462
|
+
|
3463
|
+
while current_token && current_token[:type] == :operator && current_token[:value] == '.'
|
3464
|
+
dot_count += 1
|
3465
|
+
#puts "Found dot #{dot_count}"
|
3466
|
+
advance
|
3467
|
+
end
|
3468
|
+
|
3469
|
+
if dot_count == 3
|
3470
|
+
#puts "Complete varargs found (3 dots)"
|
3471
|
+
params << {
|
3472
|
+
type: :var_args_legacy,
|
3473
|
+
name: '...',
|
3474
|
+
line: dot_line, # Use stored values
|
3475
|
+
column: dot_column
|
3476
|
+
}
|
3477
|
+
else
|
3478
|
+
#puts "Incomplete varargs: only #{dot_count} dots found"
|
3479
|
+
end
|
3480
|
+
end
|
3481
|
+
break
|
3482
|
+
else
|
3483
|
+
# Add debug to see what unexpected token we're encountering
|
3484
|
+
#puts "Unexpected token in parameter list: #{current_token ? current_token[:type] : 'nil'} #{current_token ? current_token[:value] : ''}"
|
3485
|
+
|
3486
|
+
# Not a valid parameter or varargs
|
3487
|
+
@errors << {
|
3488
|
+
message: "Expected parameter name",
|
3489
|
+
position: @position,
|
3490
|
+
line: current_token ? current_token[:line] : 0,
|
3491
|
+
column: current_token ? current_token[:column] : 0
|
3492
|
+
}
|
3493
|
+
break
|
3494
|
+
end
|
3495
|
+
end
|
3496
|
+
if current_token && current_token[:type] == :comma
|
3497
|
+
advance
|
3498
|
+
else
|
3499
|
+
break
|
3500
|
+
end
|
3501
|
+
end
|
3502
|
+
|
3503
|
+
params
|
3504
|
+
end
|
3505
|
+
|
3506
|
+
def parse_try_statement
|
3507
|
+
token = advance # Skip 'try'
|
611
3508
|
line = token[:line]
|
612
3509
|
column = token[:column]
|
613
3510
|
|
614
|
-
|
615
|
-
|
3511
|
+
# Parse the try body
|
3512
|
+
body = []
|
3513
|
+
catch_clauses = []
|
3514
|
+
finally_clause = nil
|
3515
|
+
|
3516
|
+
# Parse statements in the try block
|
3517
|
+
while @position < @tokens.length
|
3518
|
+
if current_token && current_token[:type] == :keyword &&
|
3519
|
+
['catch', 'finally', 'endtry'].include?(current_token[:value])
|
3520
|
+
break
|
3521
|
+
end
|
3522
|
+
|
3523
|
+
stmt = parse_statement
|
3524
|
+
body << stmt if stmt
|
3525
|
+
end
|
3526
|
+
|
3527
|
+
# Parse catch clauses
|
3528
|
+
while @position < @tokens.length &&
|
3529
|
+
current_token && current_token[:type] == :keyword &&
|
3530
|
+
current_token[:value] == 'catch'
|
3531
|
+
catch_token = advance # Skip 'catch'
|
3532
|
+
catch_line = catch_token[:line]
|
3533
|
+
catch_column = catch_token[:column]
|
3534
|
+
|
3535
|
+
# Parse the pattern (anything until the next statement)
|
3536
|
+
pattern = ''
|
3537
|
+
pattern_tokens = []
|
3538
|
+
|
3539
|
+
# Collect all tokens until we hit a newline or a statement
|
3540
|
+
while @position < @tokens.length
|
3541
|
+
if !current_token ||
|
3542
|
+
(current_token[:type] == :whitespace && current_token[:value].include?("\n")) ||
|
3543
|
+
current_token[:type] == :comment
|
3544
|
+
break
|
3545
|
+
end
|
3546
|
+
|
3547
|
+
pattern_tokens << current_token
|
3548
|
+
pattern += current_token[:value]
|
3549
|
+
advance
|
3550
|
+
end
|
3551
|
+
|
3552
|
+
# Parse the catch body
|
3553
|
+
catch_body = []
|
3554
|
+
while @position < @tokens.length
|
3555
|
+
if current_token && current_token[:type] == :keyword &&
|
3556
|
+
['catch', 'finally', 'endtry'].include?(current_token[:value])
|
3557
|
+
break
|
3558
|
+
end
|
3559
|
+
|
3560
|
+
stmt = parse_statement
|
3561
|
+
catch_body << stmt if stmt
|
3562
|
+
end
|
3563
|
+
|
3564
|
+
catch_clauses << {
|
3565
|
+
type: :catch_clause,
|
3566
|
+
pattern: pattern.strip,
|
3567
|
+
body: catch_body,
|
3568
|
+
line: catch_line,
|
3569
|
+
column: catch_column
|
3570
|
+
}
|
3571
|
+
end
|
3572
|
+
|
3573
|
+
# Parse finally clause if present
|
3574
|
+
if @position < @tokens.length &&
|
3575
|
+
current_token && current_token[:type] == :keyword &&
|
3576
|
+
current_token[:value] == 'finally'
|
3577
|
+
finally_token = advance # Skip 'finally'
|
3578
|
+
finally_line = finally_token[:line]
|
3579
|
+
finally_column = finally_token[:column]
|
3580
|
+
|
3581
|
+
# Parse the finally body
|
3582
|
+
finally_body = []
|
3583
|
+
while @position < @tokens.length
|
3584
|
+
if current_token && current_token[:type] == :keyword && current_token[:value] == 'endtry'
|
3585
|
+
break
|
3586
|
+
end
|
3587
|
+
|
3588
|
+
stmt = parse_statement
|
3589
|
+
finally_body << stmt if stmt
|
3590
|
+
end
|
3591
|
+
|
3592
|
+
finally_clause = {
|
3593
|
+
type: :finally_clause,
|
3594
|
+
body: finally_body,
|
3595
|
+
line: finally_line,
|
3596
|
+
column: finally_column
|
3597
|
+
}
|
3598
|
+
end
|
3599
|
+
|
3600
|
+
# Expect endtry
|
3601
|
+
if current_token && current_token[:type] == :keyword && current_token[:value] == 'endtry'
|
3602
|
+
advance # Skip 'endtry'
|
3603
|
+
else
|
3604
|
+
# Only add an error if we haven't reached the end of the file
|
3605
|
+
if @position < @tokens.length
|
3606
|
+
@errors << {
|
3607
|
+
message: "Expected 'endtry' to close try statement",
|
3608
|
+
position: @position,
|
3609
|
+
line: current_token ? current_token[:line] : 0,
|
3610
|
+
column: current_token ? current_token[:column] : 0
|
3611
|
+
}
|
3612
|
+
end
|
3613
|
+
end
|
3614
|
+
|
3615
|
+
return {
|
3616
|
+
type: :try_statement,
|
3617
|
+
body: body,
|
3618
|
+
catch_clauses: catch_clauses,
|
3619
|
+
finally_clause: finally_clause,
|
3620
|
+
line: line,
|
3621
|
+
column: column
|
3622
|
+
}
|
3623
|
+
end
|
3624
|
+
|
3625
|
+
# Also add a method to parse throw statements:
|
3626
|
+
|
3627
|
+
def parse_throw_statement
|
3628
|
+
token = advance # Skip 'throw'
|
3629
|
+
line = token[:line]
|
3630
|
+
column = token[:column]
|
3631
|
+
|
3632
|
+
# Parse the expression to throw
|
3633
|
+
expression = parse_expression
|
3634
|
+
|
3635
|
+
return {
|
3636
|
+
type: :throw_statement,
|
3637
|
+
expression: expression,
|
3638
|
+
line: line,
|
3639
|
+
column: column
|
3640
|
+
}
|
3641
|
+
end
|
3642
|
+
|
3643
|
+
def parse_call_statement
|
3644
|
+
token = advance # Skip 'call'
|
3645
|
+
line = token[:line]
|
3646
|
+
column = token[:column]
|
3647
|
+
|
3648
|
+
# Parse the function call expression that follows 'call'
|
3649
|
+
func_expr = nil
|
3650
|
+
|
3651
|
+
if current_token && current_token[:type] == :script_local
|
3652
|
+
# Handle script-local function call (s:func_name)
|
3653
|
+
func_name = current_token[:value]
|
3654
|
+
func_line = current_token[:line]
|
3655
|
+
func_column = current_token[:column]
|
616
3656
|
advance
|
617
|
-
|
618
|
-
|
619
|
-
|
620
|
-
|
621
|
-
|
622
|
-
|
3657
|
+
|
3658
|
+
# Parse arguments
|
3659
|
+
args = []
|
3660
|
+
if current_token && current_token[:type] == :paren_open
|
3661
|
+
expect(:paren_open) # Skip '('
|
3662
|
+
|
3663
|
+
# Parse arguments if any
|
3664
|
+
unless current_token && current_token[:type] == :paren_close
|
3665
|
+
loop do
|
3666
|
+
arg = parse_expression
|
3667
|
+
args << arg if arg
|
3668
|
+
|
3669
|
+
if current_token && current_token[:type] == :comma
|
3670
|
+
advance # Skip comma
|
3671
|
+
else
|
3672
|
+
break
|
3673
|
+
end
|
3674
|
+
end
|
3675
|
+
end
|
3676
|
+
|
3677
|
+
expect(:paren_close) # Skip ')'
|
3678
|
+
end
|
3679
|
+
|
3680
|
+
func_expr = {
|
3681
|
+
type: :script_local_call,
|
3682
|
+
name: func_name,
|
3683
|
+
arguments: args,
|
3684
|
+
line: func_line,
|
3685
|
+
column: func_column
|
3686
|
+
}
|
3687
|
+
else
|
3688
|
+
# For other function calls
|
3689
|
+
func_expr = parse_expression
|
3690
|
+
end
|
3691
|
+
|
3692
|
+
{
|
3693
|
+
type: :call_statement,
|
3694
|
+
expression: func_expr,
|
3695
|
+
line: line,
|
3696
|
+
column: column
|
3697
|
+
}
|
3698
|
+
end
|
3699
|
+
|
3700
|
+
def parse_string
|
3701
|
+
# Start with the first string or expression
|
3702
|
+
left = parse_primary_term
|
3703
|
+
|
3704
|
+
# Continue as long as we see the '.' or '..' string concatenation operator
|
3705
|
+
while current_token && current_token[:type] == :operator &&
|
3706
|
+
(current_token[:value] == '.' || current_token[:value] == '..')
|
3707
|
+
|
3708
|
+
# Store the operator token
|
3709
|
+
op_token = advance # Skip the operator
|
3710
|
+
op = op_token[:value]
|
3711
|
+
|
3712
|
+
# If this is the dot operator, check if it's actually part of '..'
|
3713
|
+
if op == '.' && peek_token && peek_token[:type] == :operator && peek_token[:value] == '.'
|
3714
|
+
advance # Skip the second dot
|
3715
|
+
op = '..'
|
3716
|
+
end
|
3717
|
+
|
3718
|
+
# Parse the right side of the concatenation
|
3719
|
+
right = parse_primary_term
|
3720
|
+
|
3721
|
+
# Create a string concatenation expression
|
3722
|
+
left = {
|
3723
|
+
type: :string_concatenation,
|
3724
|
+
operator: op,
|
3725
|
+
left: left,
|
3726
|
+
right: right,
|
3727
|
+
line: op_token[:line],
|
3728
|
+
column: op_token[:column]
|
623
3729
|
}
|
3730
|
+
end
|
3731
|
+
|
3732
|
+
return left
|
3733
|
+
end
|
3734
|
+
|
3735
|
+
# Helper function to parse a primary term in string expressions
|
3736
|
+
def parse_primary_term
|
3737
|
+
if !current_token
|
3738
|
+
return nil
|
3739
|
+
end
|
3740
|
+
|
3741
|
+
token = current_token
|
3742
|
+
line = token[:line]
|
3743
|
+
column = token[:column]
|
3744
|
+
|
3745
|
+
case token[:type]
|
624
3746
|
when :string
|
3747
|
+
# Handle string literal
|
625
3748
|
advance
|
626
|
-
{
|
3749
|
+
return {
|
627
3750
|
type: :literal,
|
628
3751
|
value: token[:value],
|
629
3752
|
token_type: :string,
|
630
3753
|
line: line,
|
631
3754
|
column: column
|
632
3755
|
}
|
633
|
-
when :identifier
|
3756
|
+
when :identifier, :global_variable, :script_local, :arg_variable,
|
3757
|
+
:local_variable, :buffer_local, :window_local, :tab_local
|
3758
|
+
# Handle variable references
|
634
3759
|
advance
|
635
|
-
|
636
|
-
|
637
|
-
if current_token && current_token[:type] == :paren_open
|
638
|
-
return parse_function_call(token[:value], line, column)
|
639
|
-
end
|
640
|
-
|
641
|
-
{
|
642
|
-
type: :identifier,
|
3760
|
+
return {
|
3761
|
+
type: token[:type],
|
643
3762
|
name: token[:value],
|
644
3763
|
line: line,
|
645
3764
|
column: column
|
646
3765
|
}
|
647
3766
|
when :paren_open
|
648
|
-
|
3767
|
+
# Handle parenthesized expressions
|
3768
|
+
advance # Skip '('
|
649
3769
|
expr = parse_expression
|
650
|
-
expect(:paren_close)
|
3770
|
+
expect(:paren_close) # Skip ')'
|
651
3771
|
return expr
|
3772
|
+
when :function
|
3773
|
+
# Handle function calls
|
3774
|
+
return parse_function_call(token[:value], line, column)
|
652
3775
|
else
|
653
|
-
|
654
|
-
|
655
|
-
position: @position,
|
656
|
-
line: line,
|
657
|
-
column: column
|
658
|
-
}
|
659
|
-
advance
|
660
|
-
return nil
|
661
|
-
end
|
662
|
-
end
|
663
|
-
|
664
|
-
def parse_function_call(name, line, column)
|
665
|
-
expect(:paren_open)
|
666
|
-
|
667
|
-
args = []
|
668
|
-
|
669
|
-
# Parse arguments
|
670
|
-
unless current_token && current_token[:type] == :paren_close
|
671
|
-
loop do
|
672
|
-
arg = parse_expression
|
673
|
-
args << arg if arg
|
674
|
-
|
675
|
-
break unless current_token && current_token[:type] == :comma
|
676
|
-
advance # Skip comma
|
677
|
-
end
|
3776
|
+
# For anything else, use the standard expression parser
|
3777
|
+
return parse_expression
|
678
3778
|
end
|
679
|
-
|
680
|
-
expect(:paren_close)
|
681
|
-
|
682
|
-
{
|
683
|
-
type: :function_call,
|
684
|
-
name: name,
|
685
|
-
arguments: args,
|
686
|
-
line: line,
|
687
|
-
column: column
|
688
|
-
}
|
689
3779
|
end
|
690
3780
|
|
691
|
-
def
|
692
|
-
token = advance # Skip '
|
3781
|
+
def parse_silent_command
|
3782
|
+
token = advance # Skip 'silent'
|
693
3783
|
line = token[:line]
|
694
3784
|
column = token[:column]
|
695
3785
|
|
696
|
-
#
|
697
|
-
|
698
|
-
|
699
|
-
|
3786
|
+
# Check for ! after silent
|
3787
|
+
has_bang = false
|
3788
|
+
if current_token && current_token[:type] == :operator && current_token[:value] == '!'
|
3789
|
+
has_bang = true
|
3790
|
+
advance # Skip '!'
|
3791
|
+
end
|
700
3792
|
|
701
|
-
|
702
|
-
|
703
|
-
|
3793
|
+
# Now parse the command that follows silent
|
3794
|
+
# It could be a standard command or a command with a range
|
3795
|
+
command = nil
|
3796
|
+
|
3797
|
+
# Check if the next token is a range operator (% in this case)
|
3798
|
+
if current_token && current_token[:type] == :operator && current_token[:value] == '%'
|
3799
|
+
range_token = advance # Skip '%'
|
3800
|
+
|
3801
|
+
# Now we expect a command (like 'delete')
|
3802
|
+
if current_token &&
|
3803
|
+
(current_token[:type] == :keyword || current_token[:type] == :identifier)
|
3804
|
+
cmd_token = advance # Skip the command name
|
3805
|
+
cmd_name = cmd_token[:value]
|
3806
|
+
|
3807
|
+
# Parse any arguments to the command
|
3808
|
+
args = []
|
3809
|
+
while current_token &&
|
3810
|
+
current_token[:type] != :comment &&
|
3811
|
+
(current_token[:value] != "\n" ||
|
3812
|
+
(current_token[:value] == "\n" &&
|
3813
|
+
!current_token[:value].start_with?("\\")))
|
3814
|
+
|
3815
|
+
# Add the token as an argument
|
3816
|
+
args << current_token
|
3817
|
+
advance
|
3818
|
+
end
|
704
3819
|
|
705
|
-
|
706
|
-
|
707
|
-
|
708
|
-
|
3820
|
+
command = {
|
3821
|
+
type: :range_command,
|
3822
|
+
range: '%',
|
3823
|
+
command: {
|
3824
|
+
type: :command,
|
3825
|
+
name: cmd_name,
|
3826
|
+
args: args,
|
3827
|
+
line: cmd_token[:line],
|
3828
|
+
column: cmd_token[:column]
|
3829
|
+
},
|
3830
|
+
line: range_token[:line],
|
3831
|
+
column: range_token[:column]
|
3832
|
+
}
|
709
3833
|
else
|
710
3834
|
@errors << {
|
711
|
-
message: "Expected
|
3835
|
+
message: "Expected command after range operator '%'",
|
712
3836
|
position: @position,
|
713
|
-
line: current_token ? current_token[:line] :
|
714
|
-
column: current_token ? current_token[:column] :
|
3837
|
+
line: current_token ? current_token[:line] : range_token[:line],
|
3838
|
+
column: current_token ? current_token[:column] : range_token[:column] + 1
|
715
3839
|
}
|
716
3840
|
end
|
717
3841
|
else
|
718
|
-
#
|
719
|
-
|
720
|
-
|
3842
|
+
# Parse a regular command
|
3843
|
+
command = parse_statement
|
3844
|
+
end
|
721
3845
|
|
722
|
-
|
723
|
-
|
724
|
-
|
3846
|
+
return {
|
3847
|
+
type: :silent_command,
|
3848
|
+
has_bang: has_bang,
|
3849
|
+
command: command,
|
3850
|
+
line: line,
|
3851
|
+
column: column
|
3852
|
+
}
|
3853
|
+
end
|
725
3854
|
|
3855
|
+
def parse_delete_command
|
3856
|
+
token = advance # Skip 'delete'
|
3857
|
+
line = token[:line]
|
3858
|
+
column = token[:column]
|
3859
|
+
|
3860
|
+
# Check for register argument (could be an underscore '_')
|
3861
|
+
register = nil
|
3862
|
+
if current_token
|
3863
|
+
if current_token[:type] == :identifier && current_token[:value] == '_'
|
3864
|
+
register = current_token[:value]
|
3865
|
+
advance
|
3866
|
+
elsif current_token[:type] == :operator && current_token[:value] == '_'
|
3867
|
+
# Handle underscore as an operator (some lexers might classify it this way)
|
3868
|
+
register = current_token[:value]
|
726
3869
|
advance
|
727
|
-
else
|
728
|
-
# Handle other import formats
|
729
|
-
module_expr = parse_expression()
|
730
|
-
if module_expr && module_expr[:type] == :literal && module_expr[:token_type] == :string
|
731
|
-
path = module_expr[:value]
|
732
|
-
module_name = path.gsub(/['"]/, '').split('/').last.split('.').first
|
733
|
-
end
|
734
3870
|
end
|
735
3871
|
end
|
736
3872
|
|
737
|
-
|
738
|
-
|
739
|
-
|
740
|
-
|
3873
|
+
return {
|
3874
|
+
type: :delete_command,
|
3875
|
+
register: register,
|
3876
|
+
line: line,
|
3877
|
+
column: column
|
3878
|
+
}
|
3879
|
+
end
|
3880
|
+
|
3881
|
+
def parse_brace_sequence_value
|
3882
|
+
# Handle special brace sequences like {{{,}}} for foldmarker
|
3883
|
+
value_parts = []
|
3884
|
+
|
3885
|
+
while current_token && (current_token[:type] == :brace_open ||
|
3886
|
+
current_token[:type] == :brace_close ||
|
3887
|
+
current_token[:type] == :comma)
|
3888
|
+
value_parts << current_token[:value]
|
3889
|
+
advance
|
3890
|
+
end
|
3891
|
+
|
3892
|
+
return {
|
3893
|
+
type: :brace_sequence,
|
3894
|
+
value: value_parts.join(''),
|
3895
|
+
line: current_token ? current_token[:line] : 0,
|
3896
|
+
column: current_token ? current_token[:column] : 0
|
3897
|
+
}
|
3898
|
+
end
|
3899
|
+
|
3900
|
+
def parse_comma_separated_value
|
3901
|
+
# Handle comma-separated option values like menu,menuone,noinsert,noselect
|
3902
|
+
values = []
|
3903
|
+
|
3904
|
+
# Parse first value
|
3905
|
+
if current_token && current_token[:type] == :identifier
|
3906
|
+
values << current_token[:value]
|
3907
|
+
advance
|
3908
|
+
else
|
3909
|
+
return parse_expression # Fall back to regular expression parsing
|
3910
|
+
end
|
3911
|
+
|
3912
|
+
# Parse additional comma-separated values
|
3913
|
+
while current_token && current_token[:type] == :comma
|
3914
|
+
advance # Skip comma
|
741
3915
|
if current_token && current_token[:type] == :identifier
|
742
|
-
|
3916
|
+
values << current_token[:value]
|
3917
|
+
advance
|
743
3918
|
else
|
744
|
-
|
745
|
-
message: "Expected identifier after 'as'",
|
746
|
-
position: @position,
|
747
|
-
line: current_token ? current_token[:line] : 0,
|
748
|
-
column: current_token ? current_token[:column] : 0
|
749
|
-
}
|
3919
|
+
break
|
750
3920
|
end
|
751
3921
|
end
|
752
|
-
|
753
|
-
{
|
754
|
-
type: :
|
755
|
-
|
756
|
-
|
757
|
-
|
758
|
-
as_name: as_name,
|
759
|
-
line: line,
|
760
|
-
column: column
|
3922
|
+
|
3923
|
+
return {
|
3924
|
+
type: :comma_separated_value,
|
3925
|
+
values: values,
|
3926
|
+
line: current_token ? current_token[:line] : 0,
|
3927
|
+
column: current_token ? current_token[:column] : 0
|
761
3928
|
}
|
762
3929
|
end
|
763
|
-
|
764
|
-
|
3930
|
+
|
3931
|
+
def parse_set_command
|
3932
|
+
token = advance # Skip 'set'
|
765
3933
|
line = token[:line]
|
766
3934
|
column = token[:column]
|
767
3935
|
|
768
|
-
#
|
769
|
-
|
3936
|
+
# Parse option name
|
3937
|
+
option_name = nil
|
3938
|
+
if current_token && current_token[:type] == :identifier
|
3939
|
+
option_name = current_token[:value]
|
3940
|
+
advance
|
3941
|
+
elsif current_token && current_token[:type] == :option_variable
|
3942
|
+
# Handle option variable directly (like &option)
|
3943
|
+
option_name = current_token[:value].sub(/^&/, '')
|
3944
|
+
advance
|
3945
|
+
else
|
770
3946
|
@errors << {
|
771
|
-
message: "Expected
|
3947
|
+
message: "Expected option name after 'set'",
|
772
3948
|
position: @position,
|
3949
|
+
line: current_token ? current_token[:line] : line,
|
3950
|
+
column: current_token ? current_token[:column] : column + 3
|
3951
|
+
}
|
3952
|
+
return {
|
3953
|
+
type: :set_command,
|
3954
|
+
option: nil,
|
3955
|
+
value: nil,
|
3956
|
+
reset_to_vim_default: false,
|
773
3957
|
line: line,
|
774
3958
|
column: column
|
775
3959
|
}
|
776
|
-
return nil
|
777
3960
|
end
|
778
3961
|
|
779
|
-
|
3962
|
+
# Check for &vim suffix (reset to Vim default)
|
3963
|
+
reset_to_vim_default = false
|
3964
|
+
if current_token && current_token[:type] == :option_variable &&
|
3965
|
+
(current_token[:value] == '&vim' || current_token[:value] == '&')
|
3966
|
+
reset_to_vim_default = true
|
3967
|
+
advance
|
3968
|
+
end
|
780
3969
|
|
781
|
-
|
782
|
-
|
783
|
-
|
784
|
-
|
785
|
-
|
786
|
-
|
787
|
-
|
788
|
-
|
789
|
-
|
790
|
-
|
791
|
-
|
792
|
-
|
793
|
-
position: @position,
|
794
|
-
line: current_token[:line],
|
795
|
-
column: current_token[:column]
|
796
|
-
}
|
797
|
-
advance
|
798
|
-
return nil
|
3970
|
+
# Check for equals sign and value
|
3971
|
+
value = nil
|
3972
|
+
if current_token && current_token[:type] == :operator && current_token[:value] == '='
|
3973
|
+
advance # Skip '='
|
3974
|
+
|
3975
|
+
# Special handling for foldmarker and similar options that use brace notation
|
3976
|
+
if option_name == 'foldmarker' && current_token && current_token[:type] == :brace_open
|
3977
|
+
# Parse as special brace sequence value (e.g., {{{,}}})
|
3978
|
+
value = parse_brace_sequence_value
|
3979
|
+
# Special handling for comma-separated option values
|
3980
|
+
elsif ['completeopt', 'formatoptions', 'guioptions', 'wildoptions'].include?(option_name)
|
3981
|
+
value = parse_comma_separated_value
|
799
3982
|
else
|
800
|
-
|
801
|
-
message: "Unexpected keyword after export: #{current_token[:value]}",
|
802
|
-
position: @position,
|
803
|
-
line: current_token[:line],
|
804
|
-
column: current_token[:column]
|
805
|
-
}
|
806
|
-
advance
|
807
|
-
return nil
|
3983
|
+
value = parse_expression
|
808
3984
|
end
|
809
|
-
|
810
|
-
|
811
|
-
|
812
|
-
|
813
|
-
|
814
|
-
|
3985
|
+
elsif current_token && current_token[:type] == :operator && current_token[:value] == '-'
|
3986
|
+
# Handle syntaxes like 'set option-=value'
|
3987
|
+
op = current_token[:value]
|
3988
|
+
advance # Skip the operator
|
3989
|
+
|
3990
|
+
if current_token && current_token[:type] == :operator &&
|
3991
|
+
['+', '=', '^'].include?(current_token[:value])
|
3992
|
+
op += current_token[:value]
|
3993
|
+
advance # Skip the second part of the operator
|
3994
|
+
end
|
3995
|
+
|
3996
|
+
value = {
|
3997
|
+
type: :option_operation,
|
3998
|
+
operator: op,
|
3999
|
+
value: parse_expression,
|
4000
|
+
line: line,
|
4001
|
+
column: column
|
4002
|
+
}
|
4003
|
+
elsif current_token && current_token[:type] == :operator && current_token[:value] == '+'
|
4004
|
+
# Handle syntaxes like 'set option+=value'
|
4005
|
+
op = current_token[:value]
|
4006
|
+
advance # Skip the operator
|
4007
|
+
|
4008
|
+
if current_token && current_token[:type] == :operator && current_token[:value] == '='
|
4009
|
+
op += current_token[:value]
|
4010
|
+
advance # Skip the '=' part
|
4011
|
+
end
|
4012
|
+
|
4013
|
+
value = {
|
4014
|
+
type: :option_operation,
|
4015
|
+
operator: op,
|
4016
|
+
value: parse_expression,
|
4017
|
+
line: line,
|
4018
|
+
column: column
|
815
4019
|
}
|
4020
|
+
end
|
4021
|
+
|
4022
|
+
return {
|
4023
|
+
type: :set_command,
|
4024
|
+
option: option_name,
|
4025
|
+
value: value,
|
4026
|
+
reset_to_vim_default: reset_to_vim_default,
|
4027
|
+
line: line,
|
4028
|
+
column: column
|
4029
|
+
}
|
4030
|
+
end
|
4031
|
+
|
4032
|
+
def parse_syntax_command
|
4033
|
+
token = advance # Skip 'syntax'
|
4034
|
+
line = token[:line]
|
4035
|
+
column = token[:column]
|
4036
|
+
|
4037
|
+
# Parse syntax subcommand (match, region, keyword, etc.)
|
4038
|
+
subcommand = nil
|
4039
|
+
if current_token && current_token[:type] == :identifier
|
4040
|
+
subcommand = current_token[:value]
|
4041
|
+
advance
|
4042
|
+
end
|
4043
|
+
|
4044
|
+
# Parse syntax group name
|
4045
|
+
group_name = nil
|
4046
|
+
if current_token && current_token[:type] == :identifier
|
4047
|
+
group_name = current_token[:value]
|
4048
|
+
advance
|
4049
|
+
end
|
4050
|
+
|
4051
|
+
# Parse pattern (regex or other pattern)
|
4052
|
+
pattern = nil
|
4053
|
+
if current_token && current_token[:type] == :regex
|
4054
|
+
pattern = current_token[:value]
|
4055
|
+
advance
|
4056
|
+
end
|
4057
|
+
|
4058
|
+
# Parse any remaining arguments as raw text until end of line
|
4059
|
+
args = []
|
4060
|
+
while current_token && current_token[:type] != :comment
|
4061
|
+
args << current_token[:value]
|
816
4062
|
advance
|
817
|
-
return nil
|
818
4063
|
end
|
819
4064
|
|
820
4065
|
{
|
821
|
-
type: :
|
822
|
-
|
4066
|
+
type: :syntax_command,
|
4067
|
+
subcommand: subcommand,
|
4068
|
+
group_name: group_name,
|
4069
|
+
pattern: pattern,
|
4070
|
+
args: args.join(' ').strip,
|
823
4071
|
line: line,
|
824
4072
|
column: column
|
825
4073
|
}
|
826
4074
|
end
|
827
4075
|
|
828
|
-
def
|
829
|
-
token = advance # Skip '
|
4076
|
+
def parse_highlight_command
|
4077
|
+
token = advance # Skip 'highlight'
|
830
4078
|
line = token[:line]
|
831
4079
|
column = token[:column]
|
832
4080
|
|
833
|
-
#
|
834
|
-
|
835
|
-
if current_token && current_token[:type] == :
|
836
|
-
|
837
|
-
advance
|
4081
|
+
# Parse highlight group name
|
4082
|
+
group_name = nil
|
4083
|
+
if current_token && current_token[:type] == :identifier
|
4084
|
+
group_name = current_token[:value]
|
4085
|
+
advance
|
838
4086
|
end
|
839
4087
|
|
840
|
-
|
4088
|
+
# Parse highlight attributes as key=value pairs
|
4089
|
+
attributes = {}
|
4090
|
+
while current_token && current_token[:type] != :comment
|
4091
|
+
# Parse attribute name
|
4092
|
+
if current_token && current_token[:type] == :identifier
|
4093
|
+
attr_name = current_token[:value]
|
4094
|
+
advance
|
841
4095
|
|
842
|
-
|
843
|
-
|
844
|
-
|
845
|
-
expect(:paren_close)
|
4096
|
+
# Parse = sign
|
4097
|
+
if current_token && current_token[:type] == :operator && current_token[:value] == '='
|
4098
|
+
advance
|
846
4099
|
|
847
|
-
|
848
|
-
|
849
|
-
|
850
|
-
|
851
|
-
|
4100
|
+
# Parse attribute value (can be identifier, number, or hex color)
|
4101
|
+
if current_token && (current_token[:type] == :identifier ||
|
4102
|
+
current_token[:type] == :number ||
|
4103
|
+
current_token[:type] == :hex_color)
|
4104
|
+
attributes[attr_name] = current_token[:value]
|
4105
|
+
advance
|
4106
|
+
end
|
4107
|
+
end
|
852
4108
|
else
|
853
|
-
|
4109
|
+
advance
|
854
4110
|
end
|
855
4111
|
end
|
856
4112
|
|
857
|
-
|
858
|
-
|
859
|
-
|
860
|
-
|
861
|
-
|
862
|
-
|
863
|
-
|
4113
|
+
{
|
4114
|
+
type: :highlight_command,
|
4115
|
+
group_name: group_name,
|
4116
|
+
attributes: attributes,
|
4117
|
+
line: line,
|
4118
|
+
column: column
|
4119
|
+
}
|
4120
|
+
end
|
864
4121
|
|
865
|
-
|
866
|
-
|
867
|
-
|
4122
|
+
def parse_sleep_command
|
4123
|
+
token = advance # Skip 'sleep'
|
4124
|
+
line = token[:line]
|
4125
|
+
column = token[:column]
|
868
4126
|
|
869
|
-
#
|
870
|
-
|
871
|
-
if
|
872
|
-
|
4127
|
+
# Parse duration (number with optional suffix)
|
4128
|
+
duration = nil
|
4129
|
+
if current_token && current_token[:type] == :number
|
4130
|
+
duration = current_token[:value]
|
4131
|
+
advance
|
4132
|
+
else
|
873
4133
|
@errors << {
|
874
|
-
message: "Expected
|
4134
|
+
message: "Expected duration after 'sleep'",
|
875
4135
|
position: @position,
|
876
|
-
line:
|
877
|
-
column:
|
4136
|
+
line: current_token ? current_token[:line] : line,
|
4137
|
+
column: current_token ? current_token[:column] : column + 5
|
878
4138
|
}
|
879
4139
|
end
|
880
4140
|
|
881
4141
|
{
|
882
|
-
type: :
|
883
|
-
|
884
|
-
params: params,
|
885
|
-
has_bang: has_bang,
|
886
|
-
attributes: attributes,
|
887
|
-
body: body,
|
4142
|
+
type: :sleep_command,
|
4143
|
+
duration: duration,
|
888
4144
|
line: line,
|
889
4145
|
column: column
|
890
4146
|
}
|
891
4147
|
end
|
892
4148
|
|
893
|
-
|
894
|
-
|
895
|
-
|
4149
|
+
def parse_source_command
|
4150
|
+
token = advance # Skip 'source'
|
4151
|
+
line = token[:line]
|
4152
|
+
column = token[:column]
|
896
4153
|
|
897
|
-
#
|
898
|
-
|
899
|
-
|
4154
|
+
# Parse file path - collect all tokens until end of line/comment
|
4155
|
+
path_parts = []
|
4156
|
+
while current_token && current_token[:type] != :comment
|
4157
|
+
path_parts << current_token[:value]
|
4158
|
+
advance
|
900
4159
|
end
|
901
4160
|
|
902
|
-
|
903
|
-
# Check for ... (varargs) in legacy function
|
904
|
-
if current_token && current_token[:type] == :ellipsis
|
905
|
-
advance
|
906
|
-
params << { type: :var_args_legacy, name: '...' }
|
907
|
-
break
|
908
|
-
end
|
909
|
-
|
910
|
-
if !current_token || current_token[:type] != :identifier
|
911
|
-
@errors << {
|
912
|
-
message: "Expected parameter name",
|
913
|
-
position: @position,
|
914
|
-
line: current_token ? current_token[:line] : 0,
|
915
|
-
column: current_token ? current_token[:column] : 0
|
916
|
-
}
|
917
|
-
break
|
918
|
-
end
|
919
|
-
|
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
|
-
if current_token && current_token[:type] == :comma
|
930
|
-
advance
|
931
|
-
else
|
932
|
-
break
|
933
|
-
end
|
934
|
-
end
|
4161
|
+
file_path = path_parts.join('')
|
935
4162
|
|
936
|
-
|
4163
|
+
{
|
4164
|
+
type: :source_command,
|
4165
|
+
file_path: file_path,
|
4166
|
+
line: line,
|
4167
|
+
column: column
|
4168
|
+
}
|
937
4169
|
end
|
938
|
-
|
939
4170
|
end
|
940
4171
|
end
|