@sshadows/tree-sitter-al 3.3.0 → 3.3.1
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.
- package/package.json +1 -1
- package/src/parser.c +1 -1
- package/src/scanner.c +146 -117
- package/tree-sitter-al.wasm +0 -0
package/package.json
CHANGED
package/src/parser.c
CHANGED
|
@@ -722358,7 +722358,7 @@ TS_PUBLIC const TSLanguage *tree_sitter_al(void) {
|
|
|
722358
722358
|
.metadata = {
|
|
722359
722359
|
.major_version = 3,
|
|
722360
722360
|
.minor_version = 3,
|
|
722361
|
-
.patch_version =
|
|
722361
|
+
.patch_version = 1,
|
|
722362
722362
|
},
|
|
722363
722363
|
};
|
|
722364
722364
|
return &language;
|
package/src/scanner.c
CHANGED
|
@@ -71,46 +71,92 @@ static bool read_keyword_ci(TSLexer *lexer, const char *keyword) {
|
|
|
71
71
|
return true;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
74
|
+
// Consume a comment beginning at the current '/'. The '/' is consumed either
|
|
75
|
+
// way; the return value says whether it actually opened a comment, so a caller
|
|
76
|
+
// that cannot tolerate a bare '/' can decline. AL block comments do not nest
|
|
77
|
+
// (grammar.js's multiline_comment is the classic non-nesting C form).
|
|
78
|
+
static bool skip_comment(TSLexer *lexer) {
|
|
79
|
+
lexer->advance(lexer, false); // past the leading '/'
|
|
80
|
+
if (lexer->lookahead == '/') {
|
|
81
|
+
while (lexer->lookahead != 0 && lexer->lookahead != '\n') {
|
|
82
|
+
lexer->advance(lexer, false);
|
|
83
|
+
}
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
if (lexer->lookahead == '*') {
|
|
87
|
+
lexer->advance(lexer, false);
|
|
88
|
+
while (lexer->lookahead != 0) {
|
|
89
|
+
if (lexer->lookahead == '*') {
|
|
90
|
+
lexer->advance(lexer, false);
|
|
91
|
+
if (lexer->lookahead == '/') {
|
|
92
|
+
lexer->advance(lexer, false);
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
lexer->advance(lexer, false);
|
|
98
|
+
}
|
|
99
|
+
return true; // unterminated block comment runs to EOF
|
|
100
|
+
}
|
|
101
|
+
return false; // a lone '/' — not a comment
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Skip whitespace WITHOUT marking it skippable.
|
|
105
|
+
//
|
|
106
|
+
// advance(lexer, true) unconditionally resets the token's START position to the
|
|
107
|
+
// current offset. That is right for LEADING whitespace, and catastrophic
|
|
108
|
+
// afterwards: once the token text has been consumed (or mark_end called), a
|
|
109
|
+
// marking skip drags the start past the end and the node collapses to zero
|
|
110
|
+
// width at the later position. Every skip that runs after the token text must
|
|
111
|
+
// use this, never skip_whitespace.
|
|
112
|
+
static void skip_whitespace_nomark(TSLexer *lexer) {
|
|
113
|
+
while (lexer->lookahead == ' ' || lexer->lookahead == '\t' ||
|
|
114
|
+
lexer->lookahead == '\r' || lexer->lookahead == '\n' ||
|
|
115
|
+
lexer->lookahead == '\f') {
|
|
82
116
|
lexer->advance(lexer, false);
|
|
83
117
|
}
|
|
84
|
-
if (is_identifier_char(lexer->lookahead)) return false;
|
|
85
|
-
return true;
|
|
86
118
|
}
|
|
87
119
|
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
//
|
|
120
|
+
// Skip whitespace and comments, without marking. Returns false if a bare '/'
|
|
121
|
+
// was hit (already consumed), which no lookahead in this scanner can make
|
|
122
|
+
// sense of.
|
|
123
|
+
static bool skip_whitespace_and_comments(TSLexer *lexer) {
|
|
124
|
+
while (true) {
|
|
125
|
+
skip_whitespace_nomark(lexer);
|
|
126
|
+
if (lexer->lookahead != '/') return true;
|
|
127
|
+
if (!skip_comment(lexer)) return false;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Directives that grammar.js declares as `extras`. Comments are extras too, but
|
|
132
|
+
// they are handled by skip_whitespace_and_comments rather than listed here.
|
|
133
|
+
// Everything transparent to the parse tree must be stepped over by a lookahead
|
|
134
|
+
// scanning for a structural directive. Keep in sync with the `extras` array.
|
|
91
135
|
static const char *const TRANSPARENT_DIRECTIVES[] = {
|
|
92
136
|
"pragma", "endregion", "region", "define", "undef", NULL,
|
|
93
137
|
};
|
|
94
138
|
|
|
95
|
-
//
|
|
139
|
+
// Target sets for peek_directive_ci_skip_extras. Bare words, no '#'.
|
|
140
|
+
static const char *const DIRECTIVE_ENDIF[] = { "endif", NULL };
|
|
141
|
+
static const char *const DIRECTIVE_ELSE_ENDIF[] = { "else", "endif", NULL };
|
|
142
|
+
|
|
143
|
+
// Skip whitespace, comments and transparent-directive lines, then test whether
|
|
144
|
+
// what follows is a '#' directive named by one of `targets`.
|
|
145
|
+
//
|
|
146
|
+
// Used when scanning ahead for split-construct patterns (PREPROC_SPLIT_BEGIN
|
|
147
|
+
// looking for #endif, PREPROC_SPLIT_END looking for #else/#endif).
|
|
96
148
|
//
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
// within one scan, so
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
|
|
149
|
+
// EVERY target is tested against a SINGLE buffered read of the directive word.
|
|
150
|
+
// Never match candidates one after another here: consuming '#' is irreversible
|
|
151
|
+
// within one scan, and so is consuming the 'end' prefix shared by "endif" and
|
|
152
|
+
// "endregion", so a failed first attempt silently destroys the later ones. An
|
|
153
|
+
// earlier `read_keyword_ci(lexer, "else") || read_keyword_ci(lexer, "endif")`
|
|
154
|
+
// in PREPROC_SPLIT_END made the "endif" arm permanently unreachable exactly
|
|
155
|
+
// this way.
|
|
156
|
+
static bool peek_directive_ci_skip_extras(TSLexer *lexer, const char *const *targets) {
|
|
103
157
|
while (true) {
|
|
104
|
-
|
|
105
|
-
if (lexer->lookahead != '#')
|
|
106
|
-
// Not '#' — try to match keyword directly
|
|
107
|
-
for (int i = 0; keyword[i] != '\0'; i++) {
|
|
108
|
-
if (towlower(lexer->lookahead) != keyword[i]) return false;
|
|
109
|
-
lexer->advance(lexer, false);
|
|
110
|
-
}
|
|
111
|
-
if (is_identifier_char(lexer->lookahead)) return false;
|
|
112
|
-
return true;
|
|
113
|
-
}
|
|
158
|
+
if (!skip_whitespace_and_comments(lexer)) return false;
|
|
159
|
+
if (lexer->lookahead != '#') return false;
|
|
114
160
|
|
|
115
161
|
lexer->advance(lexer, false);
|
|
116
162
|
// Horizontal whitespace only: '# pragma' is one directive, but '#' and a
|
|
@@ -130,8 +176,9 @@ static bool peek_keyword_ci_skip_extras(TSLexer *lexer, const char *keyword) {
|
|
|
130
176
|
if (len >= sizeof(word)) return false; // too long to be any directive
|
|
131
177
|
word[len] = '\0';
|
|
132
178
|
|
|
133
|
-
|
|
134
|
-
|
|
179
|
+
for (int i = 0; targets[i] != NULL; i++) {
|
|
180
|
+
if (strcmp(word, targets[i]) == 0) return true;
|
|
181
|
+
}
|
|
135
182
|
|
|
136
183
|
bool transparent = false;
|
|
137
184
|
for (int i = 0; TRANSPARENT_DIRECTIVES[i] != NULL; i++) {
|
|
@@ -229,19 +276,21 @@ bool tree_sitter_al_external_scanner_scan(
|
|
|
229
276
|
}
|
|
230
277
|
}
|
|
231
278
|
|
|
232
|
-
// PREPROC_SPLIT_BEGIN: 'begin' at depth > 0, before #endif (possibly with
|
|
279
|
+
// PREPROC_SPLIT_BEGIN: 'begin' at depth > 0, before #endif (possibly with
|
|
280
|
+
// comments or transparent directive lines between)
|
|
233
281
|
//
|
|
234
|
-
// '#' handling:
|
|
235
|
-
//
|
|
236
|
-
// read_keyword_ci("if"/"endif"). These are DIFFERENT
|
|
282
|
+
// '#' handling: peek_directive_ci_skip_extras takes BARE directive words and
|
|
283
|
+
// consumes the '#' itself. PREPROC_OPEN/CLOSE manually advance past '#'
|
|
284
|
+
// before calling read_keyword_ci("if"/"endif"). These are DIFFERENT
|
|
285
|
+
// conventions — do not mix.
|
|
237
286
|
//
|
|
238
|
-
// #pragma
|
|
239
|
-
//
|
|
287
|
+
// Comments, #pragma, #region, #define and friends are all extras, hence all
|
|
288
|
+
// transparent here (see skip_whitespace_and_comments/TRANSPARENT_DIRECTIVES).
|
|
240
289
|
if (valid_symbols[PREPROC_SPLIT_BEGIN] && state->depth > 0) {
|
|
241
290
|
skip_whitespace(lexer);
|
|
242
291
|
if (read_keyword_ci(lexer, "begin")) {
|
|
243
292
|
lexer->mark_end(lexer); // token covers only 'begin'
|
|
244
|
-
if (
|
|
293
|
+
if (peek_directive_ci_skip_extras(lexer, DIRECTIVE_ENDIF)) {
|
|
245
294
|
lexer->result_symbol = PREPROC_SPLIT_BEGIN;
|
|
246
295
|
return true;
|
|
247
296
|
}
|
|
@@ -259,26 +308,17 @@ bool tree_sitter_al_external_scanner_scan(
|
|
|
259
308
|
skip_whitespace(lexer);
|
|
260
309
|
if (read_keyword_ci(lexer, "end")) {
|
|
261
310
|
lexer->mark_end(lexer); // token covers only 'end'
|
|
262
|
-
// Check for ';' then
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
311
|
+
// Check for ';' then #else or #endif. Comments and transparent directive
|
|
312
|
+
// lines may sit at either gap and must not stop the lookahead — before
|
|
313
|
+
// this skipped nothing, a single trailing `// note` after the `end;`
|
|
314
|
+
// silently dropped the token and the run reparsed as a call_statement
|
|
315
|
+
// with NO error nodes.
|
|
316
|
+
if (!skip_whitespace_and_comments(lexer)) return false;
|
|
268
317
|
if (lexer->lookahead == ';') {
|
|
269
318
|
lexer->advance(lexer, false);
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
lexer->lookahead == '\f') {
|
|
274
|
-
lexer->advance(lexer, false);
|
|
275
|
-
}
|
|
276
|
-
if (lexer->lookahead == '#') {
|
|
277
|
-
lexer->advance(lexer, false);
|
|
278
|
-
if (read_keyword_ci(lexer, "else") || read_keyword_ci(lexer, "endif")) {
|
|
279
|
-
lexer->result_symbol = PREPROC_SPLIT_END;
|
|
280
|
-
return true;
|
|
281
|
-
}
|
|
319
|
+
if (peek_directive_ci_skip_extras(lexer, DIRECTIVE_ELSE_ENDIF)) {
|
|
320
|
+
lexer->result_symbol = PREPROC_SPLIT_END;
|
|
321
|
+
return true;
|
|
282
322
|
}
|
|
283
323
|
}
|
|
284
324
|
// 'end' found but not followed by ; then #else/#endif — return false.
|
|
@@ -299,9 +339,9 @@ bool tree_sitter_al_external_scanner_scan(
|
|
|
299
339
|
lexer->mark_end(lexer);
|
|
300
340
|
|
|
301
341
|
// Scan past the attribute content to find the closing ']'.
|
|
302
|
-
//
|
|
342
|
+
// Bracket depth handles nesting; strings and comments are skipped whole so
|
|
343
|
+
// a ']' inside either cannot close the scan early.
|
|
303
344
|
int bracket_depth = 1;
|
|
304
|
-
int paren_depth = 0;
|
|
305
345
|
bool in_string = false;
|
|
306
346
|
|
|
307
347
|
while (bracket_depth > 0 && lexer->lookahead != 0) {
|
|
@@ -317,12 +357,14 @@ bool tree_sitter_al_external_scanner_scan(
|
|
|
317
357
|
continue;
|
|
318
358
|
}
|
|
319
359
|
} else {
|
|
360
|
+
if (lexer->lookahead == '/') {
|
|
361
|
+
// Consumes the '/' whether or not a comment opened, so the loop
|
|
362
|
+
// always makes progress.
|
|
363
|
+
skip_comment(lexer);
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
320
366
|
if (lexer->lookahead == '\'') {
|
|
321
367
|
in_string = true;
|
|
322
|
-
} else if (lexer->lookahead == '(') {
|
|
323
|
-
paren_depth++;
|
|
324
|
-
} else if (lexer->lookahead == ')') {
|
|
325
|
-
if (paren_depth > 0) paren_depth--;
|
|
326
368
|
} else if (lexer->lookahead == '[') {
|
|
327
369
|
bracket_depth++;
|
|
328
370
|
} else if (lexer->lookahead == ']') {
|
|
@@ -371,6 +413,10 @@ bool tree_sitter_al_external_scanner_scan(
|
|
|
371
413
|
continue;
|
|
372
414
|
}
|
|
373
415
|
} else {
|
|
416
|
+
if (lexer->lookahead == '/') {
|
|
417
|
+
skip_comment(lexer);
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
374
420
|
if (lexer->lookahead == '\'') {
|
|
375
421
|
inner_in_string = true;
|
|
376
422
|
} else if (lexer->lookahead == '[') {
|
|
@@ -397,33 +443,28 @@ bool tree_sitter_al_external_scanner_scan(
|
|
|
397
443
|
// (fall through to the identifier/quoted-identifier checks below)
|
|
398
444
|
}
|
|
399
445
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
if (lexer->lookahead == '"') {
|
|
407
|
-
lexer->advance(lexer, false);
|
|
408
|
-
// Skip whitespace
|
|
409
|
-
while (lexer->lookahead == ' ' || lexer->lookahead == '\t') {
|
|
410
|
-
lexer->advance(lexer, false);
|
|
411
|
-
}
|
|
412
|
-
if (lexer->lookahead == ':') {
|
|
413
|
-
lexer->result_symbol = VAR_ATTRIBUTE_OPEN;
|
|
414
|
-
return true;
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
return false;
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
if (is_identifier_start(lexer->lookahead)) {
|
|
421
|
-
// Identifier — scan it, then check for ':' (or ',' for multi-variable decls)
|
|
422
|
-
// Pattern: identifier (',' identifier)* ':'
|
|
446
|
+
// Variable declaration pattern: name (',' name)* ':' — where each name
|
|
447
|
+
// is a bare identifier or a quoted identifier, in ANY position. Handling
|
|
448
|
+
// quoted and bare names in one loop is what lets a quoted name lead a
|
|
449
|
+
// multi-name declaration; the previous split branches accepted a quoted
|
|
450
|
+
// name only when it was solo or in a later position.
|
|
451
|
+
if (lexer->lookahead == '"' || is_identifier_start(lexer->lookahead)) {
|
|
423
452
|
while (true) {
|
|
424
|
-
|
|
453
|
+
if (lexer->lookahead == '"') {
|
|
425
454
|
lexer->advance(lexer, false);
|
|
455
|
+
while (lexer->lookahead != 0 && lexer->lookahead != '"') {
|
|
456
|
+
lexer->advance(lexer, false);
|
|
457
|
+
}
|
|
458
|
+
if (lexer->lookahead != '"') return false; // unterminated
|
|
459
|
+
lexer->advance(lexer, false);
|
|
460
|
+
} else if (is_identifier_start(lexer->lookahead)) {
|
|
461
|
+
while (is_identifier_char(lexer->lookahead)) {
|
|
462
|
+
lexer->advance(lexer, false);
|
|
463
|
+
}
|
|
464
|
+
} else {
|
|
465
|
+
return false;
|
|
426
466
|
}
|
|
467
|
+
|
|
427
468
|
// Skip whitespace
|
|
428
469
|
while (lexer->lookahead == ' ' || lexer->lookahead == '\t') {
|
|
429
470
|
lexer->advance(lexer, false);
|
|
@@ -432,29 +473,12 @@ bool tree_sitter_al_external_scanner_scan(
|
|
|
432
473
|
lexer->result_symbol = VAR_ATTRIBUTE_OPEN;
|
|
433
474
|
return true;
|
|
434
475
|
}
|
|
435
|
-
if (lexer->lookahead
|
|
436
|
-
|
|
476
|
+
if (lexer->lookahead != ',') return false;
|
|
477
|
+
|
|
478
|
+
lexer->advance(lexer, false); // past the ','
|
|
479
|
+
while (lexer->lookahead == ' ' || lexer->lookahead == '\t') {
|
|
437
480
|
lexer->advance(lexer, false);
|
|
438
|
-
while (lexer->lookahead == ' ' || lexer->lookahead == '\t') {
|
|
439
|
-
lexer->advance(lexer, false);
|
|
440
|
-
}
|
|
441
|
-
if (lexer->lookahead == '"') {
|
|
442
|
-
// Quoted identifier in multi-var list
|
|
443
|
-
lexer->advance(lexer, false);
|
|
444
|
-
while (lexer->lookahead != 0 && lexer->lookahead != '"') {
|
|
445
|
-
lexer->advance(lexer, false);
|
|
446
|
-
}
|
|
447
|
-
if (lexer->lookahead == '"') lexer->advance(lexer, false);
|
|
448
|
-
while (lexer->lookahead == ' ' || lexer->lookahead == '\t') {
|
|
449
|
-
lexer->advance(lexer, false);
|
|
450
|
-
}
|
|
451
|
-
// Loop back to check for ':', ',' etc.
|
|
452
|
-
continue;
|
|
453
|
-
}
|
|
454
|
-
if (!is_identifier_start(lexer->lookahead)) return false;
|
|
455
|
-
continue;
|
|
456
481
|
}
|
|
457
|
-
return false;
|
|
458
482
|
}
|
|
459
483
|
}
|
|
460
484
|
|
|
@@ -517,10 +541,13 @@ bool tree_sitter_al_external_scanner_scan(
|
|
|
517
541
|
}
|
|
518
542
|
}
|
|
519
543
|
|
|
520
|
-
// Did not match continue_as_identifier
|
|
521
|
-
//
|
|
522
|
-
//
|
|
523
|
-
//
|
|
544
|
+
// Did not match continue_as_identifier. We can't fall through to
|
|
545
|
+
// PROPERTY_NAME because characters are already consumed and the external
|
|
546
|
+
// scanner is NOT re-entered for the same position after a false return —
|
|
547
|
+
// tree-sitter discards the advances and runs the internal lexer instead.
|
|
548
|
+
// This is only safe because the grammar never makes CONTINUE_AS_IDENTIFIER
|
|
549
|
+
// and PROPERTY_NAME valid in the same state (properties live in object and
|
|
550
|
+
// section bodies, `continue :=` in statement bodies).
|
|
524
551
|
return false;
|
|
525
552
|
}
|
|
526
553
|
|
|
@@ -547,11 +574,13 @@ bool tree_sitter_al_external_scanner_scan(
|
|
|
547
574
|
// Mark end of identifier (before whitespace/equals)
|
|
548
575
|
lexer->mark_end(lexer);
|
|
549
576
|
|
|
550
|
-
// Skip whitespace
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
577
|
+
// Skip whitespace and comments. '\n' belongs here just as much as '\r' —
|
|
578
|
+
// the leading skip above already accepts it, and alc accepts a property
|
|
579
|
+
// whose '=' sits on the next line (verified). Omitting it made
|
|
580
|
+
// `Caption\n = 'Test';` an ERROR that the compiler compiles fine.
|
|
581
|
+
// A bare '/' is not a comment and is not '=', so declining on it loses
|
|
582
|
+
// nothing.
|
|
583
|
+
if (!skip_whitespace_and_comments(lexer)) return false;
|
|
555
584
|
|
|
556
585
|
// Check for = but not :=
|
|
557
586
|
if (lexer->lookahead == '=') {
|
package/tree-sitter-al.wasm
CHANGED
|
Binary file
|