@sshadows/tree-sitter-al 3.2.1 → 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/grammar.js +27 -0
- package/package.json +1 -1
- package/queries/highlights.scm +2 -0
- package/src/grammar.json +16 -0
- package/src/node-types.json +10 -0
- package/src/parser.c +77248 -56270
- package/src/scanner.c +179 -137
- package/tree-sitter-al.wasm +0 -0
package/src/scanner.c
CHANGED
|
@@ -71,65 +71,124 @@ 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 == '*') {
|
|
82
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
|
|
83
100
|
}
|
|
84
|
-
|
|
85
|
-
return true;
|
|
101
|
+
return false; // a lone '/' — not a comment
|
|
86
102
|
}
|
|
87
103
|
|
|
88
|
-
// Skip whitespace
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
|
|
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') {
|
|
116
|
+
lexer->advance(lexer, false);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
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) {
|
|
92
124
|
while (true) {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
if (lexer
|
|
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.
|
|
135
|
+
static const char *const TRANSPARENT_DIRECTIVES[] = {
|
|
136
|
+
"pragma", "endregion", "region", "define", "undef", NULL,
|
|
137
|
+
};
|
|
138
|
+
|
|
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).
|
|
148
|
+
//
|
|
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) {
|
|
157
|
+
while (true) {
|
|
158
|
+
if (!skip_whitespace_and_comments(lexer)) return false;
|
|
159
|
+
if (lexer->lookahead != '#') return false;
|
|
160
|
+
|
|
161
|
+
lexer->advance(lexer, false);
|
|
162
|
+
// Horizontal whitespace only: '# pragma' is one directive, but '#' and a
|
|
163
|
+
// word on the NEXT line are not (matching the extras regexes' `[ \t]*`).
|
|
164
|
+
while (lexer->lookahead == ' ' || lexer->lookahead == '\t') {
|
|
165
|
+
lexer->advance(lexer, false);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Read the directive word. Longest AL directive is "endregion" (9).
|
|
169
|
+
char word[16];
|
|
170
|
+
size_t len = 0;
|
|
171
|
+
while (is_identifier_char(lexer->lookahead)) {
|
|
172
|
+
if (len < sizeof(word) - 1) word[len] = (char)towlower(lexer->lookahead);
|
|
173
|
+
len++;
|
|
174
|
+
lexer->advance(lexer, false);
|
|
175
|
+
}
|
|
176
|
+
if (len >= sizeof(word)) return false; // too long to be any directive
|
|
177
|
+
word[len] = '\0';
|
|
178
|
+
|
|
179
|
+
for (int i = 0; targets[i] != NULL; i++) {
|
|
180
|
+
if (strcmp(word, targets[i]) == 0) return true;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
bool transparent = false;
|
|
184
|
+
for (int i = 0; TRANSPARENT_DIRECTIVES[i] != NULL; i++) {
|
|
185
|
+
if (strcmp(word, TRANSPARENT_DIRECTIVES[i]) == 0) { transparent = true; break; }
|
|
186
|
+
}
|
|
187
|
+
if (!transparent) return false;
|
|
188
|
+
|
|
189
|
+
// Skip the rest of this directive's line, then look again.
|
|
190
|
+
while (lexer->lookahead != '\0' && lexer->lookahead != '\n') {
|
|
96
191
|
lexer->advance(lexer, false);
|
|
97
|
-
// Check if 'pragma' follows
|
|
98
|
-
const char *pragma = "pragma";
|
|
99
|
-
bool is_pragma = true;
|
|
100
|
-
for (int i = 0; pragma[i] != '\0'; i++) {
|
|
101
|
-
if (towlower(lexer->lookahead) != pragma[i]) { is_pragma = false; break; }
|
|
102
|
-
lexer->advance(lexer, false);
|
|
103
|
-
}
|
|
104
|
-
if (is_pragma && (lexer->lookahead == ' ' || lexer->lookahead == '\t' ||
|
|
105
|
-
lexer->lookahead == '\r' || lexer->lookahead == '\n' ||
|
|
106
|
-
lexer->lookahead == '\0')) {
|
|
107
|
-
// Skip rest of this #pragma line
|
|
108
|
-
while (lexer->lookahead != '\0' && lexer->lookahead != '\n') {
|
|
109
|
-
lexer->advance(lexer, false);
|
|
110
|
-
}
|
|
111
|
-
continue; // loop back to skip more whitespace/#pragma lines
|
|
112
|
-
} else {
|
|
113
|
-
// '#' was followed by something other than 'pragma' — check for keyword
|
|
114
|
-
// We already consumed '#'; now match rest of keyword (which starts with '#')
|
|
115
|
-
// Since we consumed '#', match from position 1 of keyword
|
|
116
|
-
if (keyword[0] != '#') return false;
|
|
117
|
-
const char *rest = keyword + 1;
|
|
118
|
-
for (int i = 0; rest[i] != '\0'; i++) {
|
|
119
|
-
if (towlower(lexer->lookahead) != rest[i]) return false;
|
|
120
|
-
lexer->advance(lexer, false);
|
|
121
|
-
}
|
|
122
|
-
if (is_identifier_char(lexer->lookahead)) return false;
|
|
123
|
-
return true;
|
|
124
|
-
}
|
|
125
|
-
} else {
|
|
126
|
-
// Not '#' — try to match keyword directly
|
|
127
|
-
for (int i = 0; keyword[i] != '\0'; i++) {
|
|
128
|
-
if (towlower(lexer->lookahead) != keyword[i]) return false;
|
|
129
|
-
lexer->advance(lexer, false);
|
|
130
|
-
}
|
|
131
|
-
if (is_identifier_char(lexer->lookahead)) return false;
|
|
132
|
-
return true;
|
|
133
192
|
}
|
|
134
193
|
}
|
|
135
194
|
}
|
|
@@ -217,18 +276,21 @@ bool tree_sitter_al_external_scanner_scan(
|
|
|
217
276
|
}
|
|
218
277
|
}
|
|
219
278
|
|
|
220
|
-
// 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)
|
|
221
281
|
//
|
|
222
|
-
// '#' handling:
|
|
223
|
-
//
|
|
224
|
-
// 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.
|
|
225
286
|
//
|
|
226
|
-
// #pragma
|
|
287
|
+
// Comments, #pragma, #region, #define and friends are all extras, hence all
|
|
288
|
+
// transparent here (see skip_whitespace_and_comments/TRANSPARENT_DIRECTIVES).
|
|
227
289
|
if (valid_symbols[PREPROC_SPLIT_BEGIN] && state->depth > 0) {
|
|
228
290
|
skip_whitespace(lexer);
|
|
229
291
|
if (read_keyword_ci(lexer, "begin")) {
|
|
230
292
|
lexer->mark_end(lexer); // token covers only 'begin'
|
|
231
|
-
if (
|
|
293
|
+
if (peek_directive_ci_skip_extras(lexer, DIRECTIVE_ENDIF)) {
|
|
232
294
|
lexer->result_symbol = PREPROC_SPLIT_BEGIN;
|
|
233
295
|
return true;
|
|
234
296
|
}
|
|
@@ -246,26 +308,17 @@ bool tree_sitter_al_external_scanner_scan(
|
|
|
246
308
|
skip_whitespace(lexer);
|
|
247
309
|
if (read_keyword_ci(lexer, "end")) {
|
|
248
310
|
lexer->mark_end(lexer); // token covers only 'end'
|
|
249
|
-
// Check for ';' then
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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;
|
|
255
317
|
if (lexer->lookahead == ';') {
|
|
256
318
|
lexer->advance(lexer, false);
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
lexer->lookahead == '\f') {
|
|
261
|
-
lexer->advance(lexer, false);
|
|
262
|
-
}
|
|
263
|
-
if (lexer->lookahead == '#') {
|
|
264
|
-
lexer->advance(lexer, false);
|
|
265
|
-
if (read_keyword_ci(lexer, "else") || read_keyword_ci(lexer, "endif")) {
|
|
266
|
-
lexer->result_symbol = PREPROC_SPLIT_END;
|
|
267
|
-
return true;
|
|
268
|
-
}
|
|
319
|
+
if (peek_directive_ci_skip_extras(lexer, DIRECTIVE_ELSE_ENDIF)) {
|
|
320
|
+
lexer->result_symbol = PREPROC_SPLIT_END;
|
|
321
|
+
return true;
|
|
269
322
|
}
|
|
270
323
|
}
|
|
271
324
|
// 'end' found but not followed by ; then #else/#endif — return false.
|
|
@@ -286,9 +339,9 @@ bool tree_sitter_al_external_scanner_scan(
|
|
|
286
339
|
lexer->mark_end(lexer);
|
|
287
340
|
|
|
288
341
|
// Scan past the attribute content to find the closing ']'.
|
|
289
|
-
//
|
|
342
|
+
// Bracket depth handles nesting; strings and comments are skipped whole so
|
|
343
|
+
// a ']' inside either cannot close the scan early.
|
|
290
344
|
int bracket_depth = 1;
|
|
291
|
-
int paren_depth = 0;
|
|
292
345
|
bool in_string = false;
|
|
293
346
|
|
|
294
347
|
while (bracket_depth > 0 && lexer->lookahead != 0) {
|
|
@@ -304,12 +357,14 @@ bool tree_sitter_al_external_scanner_scan(
|
|
|
304
357
|
continue;
|
|
305
358
|
}
|
|
306
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
|
+
}
|
|
307
366
|
if (lexer->lookahead == '\'') {
|
|
308
367
|
in_string = true;
|
|
309
|
-
} else if (lexer->lookahead == '(') {
|
|
310
|
-
paren_depth++;
|
|
311
|
-
} else if (lexer->lookahead == ')') {
|
|
312
|
-
if (paren_depth > 0) paren_depth--;
|
|
313
368
|
} else if (lexer->lookahead == '[') {
|
|
314
369
|
bracket_depth++;
|
|
315
370
|
} else if (lexer->lookahead == ']') {
|
|
@@ -358,6 +413,10 @@ bool tree_sitter_al_external_scanner_scan(
|
|
|
358
413
|
continue;
|
|
359
414
|
}
|
|
360
415
|
} else {
|
|
416
|
+
if (lexer->lookahead == '/') {
|
|
417
|
+
skip_comment(lexer);
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
361
420
|
if (lexer->lookahead == '\'') {
|
|
362
421
|
inner_in_string = true;
|
|
363
422
|
} else if (lexer->lookahead == '[') {
|
|
@@ -384,33 +443,28 @@ bool tree_sitter_al_external_scanner_scan(
|
|
|
384
443
|
// (fall through to the identifier/quoted-identifier checks below)
|
|
385
444
|
}
|
|
386
445
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
if (lexer->lookahead == '"') {
|
|
394
|
-
lexer->advance(lexer, false);
|
|
395
|
-
// Skip whitespace
|
|
396
|
-
while (lexer->lookahead == ' ' || lexer->lookahead == '\t') {
|
|
397
|
-
lexer->advance(lexer, false);
|
|
398
|
-
}
|
|
399
|
-
if (lexer->lookahead == ':') {
|
|
400
|
-
lexer->result_symbol = VAR_ATTRIBUTE_OPEN;
|
|
401
|
-
return true;
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
return false;
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
if (is_identifier_start(lexer->lookahead)) {
|
|
408
|
-
// Identifier — scan it, then check for ':' (or ',' for multi-variable decls)
|
|
409
|
-
// 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)) {
|
|
410
452
|
while (true) {
|
|
411
|
-
|
|
453
|
+
if (lexer->lookahead == '"') {
|
|
412
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;
|
|
413
466
|
}
|
|
467
|
+
|
|
414
468
|
// Skip whitespace
|
|
415
469
|
while (lexer->lookahead == ' ' || lexer->lookahead == '\t') {
|
|
416
470
|
lexer->advance(lexer, false);
|
|
@@ -419,29 +473,12 @@ bool tree_sitter_al_external_scanner_scan(
|
|
|
419
473
|
lexer->result_symbol = VAR_ATTRIBUTE_OPEN;
|
|
420
474
|
return true;
|
|
421
475
|
}
|
|
422
|
-
if (lexer->lookahead
|
|
423
|
-
|
|
476
|
+
if (lexer->lookahead != ',') return false;
|
|
477
|
+
|
|
478
|
+
lexer->advance(lexer, false); // past the ','
|
|
479
|
+
while (lexer->lookahead == ' ' || lexer->lookahead == '\t') {
|
|
424
480
|
lexer->advance(lexer, false);
|
|
425
|
-
while (lexer->lookahead == ' ' || lexer->lookahead == '\t') {
|
|
426
|
-
lexer->advance(lexer, false);
|
|
427
|
-
}
|
|
428
|
-
if (lexer->lookahead == '"') {
|
|
429
|
-
// Quoted identifier in multi-var list
|
|
430
|
-
lexer->advance(lexer, false);
|
|
431
|
-
while (lexer->lookahead != 0 && lexer->lookahead != '"') {
|
|
432
|
-
lexer->advance(lexer, false);
|
|
433
|
-
}
|
|
434
|
-
if (lexer->lookahead == '"') lexer->advance(lexer, false);
|
|
435
|
-
while (lexer->lookahead == ' ' || lexer->lookahead == '\t') {
|
|
436
|
-
lexer->advance(lexer, false);
|
|
437
|
-
}
|
|
438
|
-
// Loop back to check for ':', ',' etc.
|
|
439
|
-
continue;
|
|
440
|
-
}
|
|
441
|
-
if (!is_identifier_start(lexer->lookahead)) return false;
|
|
442
|
-
continue;
|
|
443
481
|
}
|
|
444
|
-
return false;
|
|
445
482
|
}
|
|
446
483
|
}
|
|
447
484
|
|
|
@@ -504,10 +541,13 @@ bool tree_sitter_al_external_scanner_scan(
|
|
|
504
541
|
}
|
|
505
542
|
}
|
|
506
543
|
|
|
507
|
-
// Did not match continue_as_identifier
|
|
508
|
-
//
|
|
509
|
-
//
|
|
510
|
-
//
|
|
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).
|
|
511
551
|
return false;
|
|
512
552
|
}
|
|
513
553
|
|
|
@@ -534,11 +574,13 @@ bool tree_sitter_al_external_scanner_scan(
|
|
|
534
574
|
// Mark end of identifier (before whitespace/equals)
|
|
535
575
|
lexer->mark_end(lexer);
|
|
536
576
|
|
|
537
|
-
// Skip whitespace
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
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;
|
|
542
584
|
|
|
543
585
|
// Check for = but not :=
|
|
544
586
|
if (lexer->lookahead == '=') {
|
package/tree-sitter-al.wasm
CHANGED
|
Binary file
|