@words-lang/parser 0.1.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.
- package/dist/analyser/analyser.d.ts +106 -0
- package/dist/analyser/analyser.d.ts.map +1 -0
- package/dist/analyser/analyser.js +291 -0
- package/dist/analyser/analyser.js.map +1 -0
- package/dist/analyser/diagnostics.d.ts +166 -0
- package/dist/analyser/diagnostics.d.ts.map +1 -0
- package/dist/analyser/diagnostics.js +139 -0
- package/dist/analyser/diagnostics.js.map +1 -0
- package/dist/analyser/workspace.d.ts +198 -0
- package/dist/analyser/workspace.d.ts.map +1 -0
- package/dist/analyser/workspace.js +403 -0
- package/dist/analyser/workspace.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/dist/lexer/lexer.d.ts +120 -0
- package/dist/lexer/lexer.d.ts.map +1 -0
- package/dist/lexer/lexer.js +365 -0
- package/dist/lexer/lexer.js.map +1 -0
- package/dist/lexer/token.d.ts +247 -0
- package/dist/lexer/token.d.ts.map +1 -0
- package/dist/lexer/token.js +250 -0
- package/dist/lexer/token.js.map +1 -0
- package/dist/parser/ast.d.ts +685 -0
- package/dist/parser/ast.d.ts.map +1 -0
- package/dist/parser/ast.js +3 -0
- package/dist/parser/ast.js.map +1 -0
- package/dist/parser/parser.d.ts +411 -0
- package/dist/parser/parser.d.ts.map +1 -0
- package/dist/parser/parser.js +1600 -0
- package/dist/parser/parser.js.map +1 -0
- package/package.json +23 -0
- package/src/analyser/analyser.ts +403 -0
- package/src/analyser/diagnostics.ts +232 -0
- package/src/analyser/workspace.ts +457 -0
- package/src/index.ts +7 -0
- package/src/lexer/lexer.ts +379 -0
- package/src/lexer/token.ts +331 -0
- package/src/parser/ast.ts +798 -0
- package/src/parser/parser.ts +1815 -0
|
@@ -0,0 +1,1600 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* parser.ts
|
|
4
|
+
*
|
|
5
|
+
* The WORDS parser. Consumes a flat token stream produced by the Lexer and
|
|
6
|
+
* builds an AST described in ast.ts.
|
|
7
|
+
*
|
|
8
|
+
* Design principles:
|
|
9
|
+
*
|
|
10
|
+
* - Recursive descent. Each grammar rule has a corresponding private method.
|
|
11
|
+
* Methods consume tokens and return AST nodes.
|
|
12
|
+
*
|
|
13
|
+
* - Error recovery. When an unexpected token is encountered the parser emits
|
|
14
|
+
* a diagnostic, skips tokens until it finds a safe synchronisation point
|
|
15
|
+
* (typically a newline, a closing paren, or a known top-level keyword), and
|
|
16
|
+
* continues. This means a single parse pass collects all errors in the file.
|
|
17
|
+
*
|
|
18
|
+
* - No exceptions for parse errors. All problems are collected in `this.diagnostics`
|
|
19
|
+
* and returned alongside the partial AST in the `ParseResult`.
|
|
20
|
+
*
|
|
21
|
+
* - Comments and newlines are consumed transparently by the `skip()` helper
|
|
22
|
+
* unless the calling rule explicitly needs them (e.g. ownership declaration
|
|
23
|
+
* detection requires newlines).
|
|
24
|
+
*
|
|
25
|
+
* - `system` and `state` are both keywords and identifier prefixes in access
|
|
26
|
+
* expressions (`system.setContext`, `state.context`). The parser disambiguates
|
|
27
|
+
* by context — inside a `uses` block or argument value position, these are
|
|
28
|
+
* treated as access expression roots, not construct keywords.
|
|
29
|
+
*/
|
|
30
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
31
|
+
exports.Parser = void 0;
|
|
32
|
+
const token_1 = require("../lexer/token");
|
|
33
|
+
const diagnostics_1 = require("../analyser/diagnostics");
|
|
34
|
+
// ── Parser ────────────────────────────────────────────────────────────────────
|
|
35
|
+
class Parser {
|
|
36
|
+
constructor(tokens) {
|
|
37
|
+
/** Current position in the token stream. */
|
|
38
|
+
this.pos = 0;
|
|
39
|
+
/** Diagnostics collected during this parse. */
|
|
40
|
+
this.diagnostics = [];
|
|
41
|
+
this.tokens = tokens;
|
|
42
|
+
}
|
|
43
|
+
// ── Public API ─────────────────────────────────────────────────────────────
|
|
44
|
+
/**
|
|
45
|
+
* Parses the entire token stream and returns a ParseResult containing
|
|
46
|
+
* the document AST and all collected diagnostics.
|
|
47
|
+
* Never throws — all errors are collected as diagnostics.
|
|
48
|
+
*/
|
|
49
|
+
parse() {
|
|
50
|
+
const document = this.parseDocument();
|
|
51
|
+
return { document, diagnostics: this.diagnostics };
|
|
52
|
+
}
|
|
53
|
+
// ── Document ───────────────────────────────────────────────────────────────
|
|
54
|
+
/**
|
|
55
|
+
* Parses a complete `.wds` file.
|
|
56
|
+
*
|
|
57
|
+
* Detects the ownership declaration pattern — a bare `module ModuleName`
|
|
58
|
+
* on its own line at the top of component files. If the first non-trivial
|
|
59
|
+
* content is `module PascalIdent Newline` (with no opening `(`), it is
|
|
60
|
+
* captured as `ownerModule` and the next construct is parsed normally.
|
|
61
|
+
*/
|
|
62
|
+
parseDocument() {
|
|
63
|
+
this.skipComments();
|
|
64
|
+
let ownerModule = null;
|
|
65
|
+
// Detect ownership declaration: module ModuleName \n (no body follows)
|
|
66
|
+
if (this.check(token_1.TokenType.Module)) {
|
|
67
|
+
const saved = this.pos;
|
|
68
|
+
this.advance(); // consume 'module'
|
|
69
|
+
this.skipComments();
|
|
70
|
+
if (this.check(token_1.TokenType.PascalIdent)) {
|
|
71
|
+
const name = this.current().value;
|
|
72
|
+
this.advance(); // consume name
|
|
73
|
+
this.skipComments();
|
|
74
|
+
// If the next meaningful token is a newline or EOF (not a string or '('),
|
|
75
|
+
// this is an ownership declaration, not a module definition.
|
|
76
|
+
if (this.check(token_1.TokenType.Newline) || this.check(token_1.TokenType.EOF)) {
|
|
77
|
+
ownerModule = name;
|
|
78
|
+
this.skipTrivia();
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
// Not an ownership declaration — restore and parse normally.
|
|
82
|
+
this.pos = saved;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
this.pos = saved;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const nodes = [];
|
|
90
|
+
while (!this.check(token_1.TokenType.EOF)) {
|
|
91
|
+
this.skipTrivia();
|
|
92
|
+
if (this.check(token_1.TokenType.EOF))
|
|
93
|
+
break;
|
|
94
|
+
const savedPos = this.pos;
|
|
95
|
+
const node = this.parseTopLevel();
|
|
96
|
+
if (node !== null) {
|
|
97
|
+
nodes.push(node);
|
|
98
|
+
}
|
|
99
|
+
else if (this.pos === savedPos) {
|
|
100
|
+
// parseTopLevel returned null without advancing (e.g. synchronise()
|
|
101
|
+
// stopped at an RParen that has no matching open at the top level).
|
|
102
|
+
// Force progress to prevent an infinite loop.
|
|
103
|
+
this.advance();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return { kind: 'Document', ownerModule, nodes };
|
|
107
|
+
}
|
|
108
|
+
// ── Top-level dispatch ─────────────────────────────────────────────────────
|
|
109
|
+
/**
|
|
110
|
+
* Parses one top-level construct and returns it, or emits a diagnostic
|
|
111
|
+
* and returns null if the current token does not start a known construct.
|
|
112
|
+
*/
|
|
113
|
+
parseTopLevel() {
|
|
114
|
+
const tok = this.current();
|
|
115
|
+
switch (tok.type) {
|
|
116
|
+
case token_1.TokenType.System: return this.parseSystem();
|
|
117
|
+
case token_1.TokenType.Module: return this.parseModule();
|
|
118
|
+
case token_1.TokenType.State: return this.parseState();
|
|
119
|
+
case token_1.TokenType.Context: return this.parseContext();
|
|
120
|
+
case token_1.TokenType.Screen: return this.parseScreen();
|
|
121
|
+
case token_1.TokenType.View: return this.parseView();
|
|
122
|
+
case token_1.TokenType.Provider: return this.parseProvider();
|
|
123
|
+
case token_1.TokenType.Adapter: return this.parseAdapter();
|
|
124
|
+
case token_1.TokenType.Interface: return this.parseInterface();
|
|
125
|
+
default:
|
|
126
|
+
this.error(diagnostics_1.DiagnosticCode.P_INVALID_CONSTRUCT_POSITION, `Unexpected token '${tok.value}' — expected a construct keyword (system, module, state, context, screen, view, provider, adapter, interface)`, tok);
|
|
127
|
+
this.synchronise();
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// ── System ─────────────────────────────────────────────────────────────────
|
|
132
|
+
/**
|
|
133
|
+
* Parses:
|
|
134
|
+
* system SystemName "description" (
|
|
135
|
+
* modules ( ModuleOne ModuleTwo )
|
|
136
|
+
* interface ( ... )
|
|
137
|
+
* )
|
|
138
|
+
*/
|
|
139
|
+
parseSystem() {
|
|
140
|
+
const tok = this.expect(token_1.TokenType.System);
|
|
141
|
+
this.skipTrivia();
|
|
142
|
+
const name = this.expectIdent('system name');
|
|
143
|
+
this.skipTrivia();
|
|
144
|
+
const description = this.parseOptionalString();
|
|
145
|
+
this.skipTrivia();
|
|
146
|
+
this.expect(token_1.TokenType.LParen);
|
|
147
|
+
const modules = [];
|
|
148
|
+
const interfaceMethods = [];
|
|
149
|
+
while (!this.check(token_1.TokenType.RParen) && !this.check(token_1.TokenType.EOF)) {
|
|
150
|
+
this.skipTrivia();
|
|
151
|
+
if (this.check(token_1.TokenType.Modules)) {
|
|
152
|
+
this.advance();
|
|
153
|
+
this.skipTrivia();
|
|
154
|
+
this.expect(token_1.TokenType.LParen);
|
|
155
|
+
while (!this.check(token_1.TokenType.RParen) && !this.check(token_1.TokenType.EOF)) {
|
|
156
|
+
this.skipTrivia();
|
|
157
|
+
if (this.check(token_1.TokenType.PascalIdent)) {
|
|
158
|
+
modules.push(this.advance().value);
|
|
159
|
+
}
|
|
160
|
+
else if (!this.check(token_1.TokenType.RParen)) {
|
|
161
|
+
this.error(diagnostics_1.DiagnosticCode.P_MISSING_IDENTIFIER, `Expected module name`, this.current());
|
|
162
|
+
this.advance();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
this.expect(token_1.TokenType.RParen);
|
|
166
|
+
}
|
|
167
|
+
else if (this.check(token_1.TokenType.Interface)) {
|
|
168
|
+
this.advance();
|
|
169
|
+
this.skipTrivia();
|
|
170
|
+
this.expect(token_1.TokenType.LParen);
|
|
171
|
+
while (!this.check(token_1.TokenType.RParen) && !this.check(token_1.TokenType.EOF)) {
|
|
172
|
+
this.skipTrivia();
|
|
173
|
+
if (this.check(token_1.TokenType.CamelIdent)) {
|
|
174
|
+
interfaceMethods.push(this.parseMethod());
|
|
175
|
+
}
|
|
176
|
+
else if (!this.check(token_1.TokenType.RParen)) {
|
|
177
|
+
this.advance();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
this.expect(token_1.TokenType.RParen);
|
|
181
|
+
}
|
|
182
|
+
else if (!this.check(token_1.TokenType.RParen)) {
|
|
183
|
+
this.advance();
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
this.expect(token_1.TokenType.RParen);
|
|
187
|
+
return { kind: 'System', token: tok, name, description, modules, interfaceMethods };
|
|
188
|
+
}
|
|
189
|
+
// ── Module ─────────────────────────────────────────────────────────────────
|
|
190
|
+
/**
|
|
191
|
+
* Parses:
|
|
192
|
+
* module ModuleName "description" (
|
|
193
|
+
* process ... ( when ... )
|
|
194
|
+
* start StateName
|
|
195
|
+
* implements Module.Handler ( methodName param(Type) ( if ... ) )
|
|
196
|
+
* system.Module.subscribeRoute ...
|
|
197
|
+
* interface HandlerName ( ... )
|
|
198
|
+
* )
|
|
199
|
+
*/
|
|
200
|
+
parseModule() {
|
|
201
|
+
const tok = this.expect(token_1.TokenType.Module);
|
|
202
|
+
this.skipTrivia();
|
|
203
|
+
const name = this.expectIdent('module name');
|
|
204
|
+
this.skipTrivia();
|
|
205
|
+
const description = this.parseOptionalString();
|
|
206
|
+
this.skipTrivia();
|
|
207
|
+
this.expect(token_1.TokenType.LParen);
|
|
208
|
+
const processes = [];
|
|
209
|
+
let startState = null;
|
|
210
|
+
const implementations = [];
|
|
211
|
+
const subscriptions = [];
|
|
212
|
+
const inlineInterfaces = [];
|
|
213
|
+
while (!this.check(token_1.TokenType.RParen) && !this.check(token_1.TokenType.EOF)) {
|
|
214
|
+
this.skipTrivia();
|
|
215
|
+
if (this.check(token_1.TokenType.Process)) {
|
|
216
|
+
processes.push(this.parseProcess());
|
|
217
|
+
}
|
|
218
|
+
else if (this.check(token_1.TokenType.Start)) {
|
|
219
|
+
this.advance();
|
|
220
|
+
this.skipTrivia();
|
|
221
|
+
startState = this.expectIdent('start state name');
|
|
222
|
+
}
|
|
223
|
+
else if (this.check(token_1.TokenType.Implements)) {
|
|
224
|
+
implementations.push(this.parseImplements());
|
|
225
|
+
}
|
|
226
|
+
else if (this.check(token_1.TokenType.Interface)) {
|
|
227
|
+
inlineInterfaces.push(this.parseInterface());
|
|
228
|
+
}
|
|
229
|
+
else if (this.checkSystemCall()) {
|
|
230
|
+
subscriptions.push(this.parseSystemCall());
|
|
231
|
+
}
|
|
232
|
+
else if (!this.check(token_1.TokenType.RParen)) {
|
|
233
|
+
this.advance();
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
this.expect(token_1.TokenType.RParen);
|
|
237
|
+
return {
|
|
238
|
+
kind: 'Module', token: tok, name, description,
|
|
239
|
+
processes, startState,
|
|
240
|
+
implements: implementations,
|
|
241
|
+
subscriptions, inlineInterfaces,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
// ── Process ────────────────────────────────────────────────────────────────
|
|
245
|
+
/**
|
|
246
|
+
* Parses:
|
|
247
|
+
* process ProcessName "description" (
|
|
248
|
+
* when State returns Context
|
|
249
|
+
* enter NextState "narrative"
|
|
250
|
+
* ...
|
|
251
|
+
* )
|
|
252
|
+
*/
|
|
253
|
+
parseProcess() {
|
|
254
|
+
const tok = this.expect(token_1.TokenType.Process);
|
|
255
|
+
this.skipTrivia();
|
|
256
|
+
const name = this.expectIdent('process name');
|
|
257
|
+
this.skipTrivia();
|
|
258
|
+
const description = this.parseOptionalString();
|
|
259
|
+
this.skipTrivia();
|
|
260
|
+
this.expect(token_1.TokenType.LParen);
|
|
261
|
+
const rules = [];
|
|
262
|
+
while (!this.check(token_1.TokenType.RParen) && !this.check(token_1.TokenType.EOF)) {
|
|
263
|
+
this.skipTrivia();
|
|
264
|
+
if (this.check(token_1.TokenType.When)) {
|
|
265
|
+
rules.push(this.parseWhenRule());
|
|
266
|
+
}
|
|
267
|
+
else if (!this.check(token_1.TokenType.RParen)) {
|
|
268
|
+
this.advance();
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
this.expect(token_1.TokenType.RParen);
|
|
272
|
+
return { kind: 'Process', token: tok, name, description, rules };
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Parses:
|
|
276
|
+
* when CurrentState returns ProducedContext
|
|
277
|
+
* enter NextState "narrative" ( inlineContext? )
|
|
278
|
+
*/
|
|
279
|
+
parseWhenRule() {
|
|
280
|
+
const tok = this.expect(token_1.TokenType.When);
|
|
281
|
+
this.skipTrivia();
|
|
282
|
+
const currentState = this.expectIdent('state name in when rule');
|
|
283
|
+
this.skipTrivia();
|
|
284
|
+
if (!this.check(token_1.TokenType.Returns)) {
|
|
285
|
+
this.error(diagnostics_1.DiagnosticCode.P_MISSING_RETURNS, `Expected 'returns' in when rule`, this.current());
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
this.advance();
|
|
289
|
+
}
|
|
290
|
+
this.skipTrivia();
|
|
291
|
+
const producedContext = this.expectIdent('context name in when rule');
|
|
292
|
+
this.skipTrivia();
|
|
293
|
+
if (!this.check(token_1.TokenType.Enter)) {
|
|
294
|
+
this.error(diagnostics_1.DiagnosticCode.P_MISSING_ENTER, `Expected 'enter' in when rule`, this.current());
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
this.advance();
|
|
298
|
+
}
|
|
299
|
+
this.skipTrivia();
|
|
300
|
+
const nextState = this.expectIdent('next state name in when rule');
|
|
301
|
+
this.skipTrivia();
|
|
302
|
+
const narrative = this.parseOptionalString();
|
|
303
|
+
this.skipTrivia();
|
|
304
|
+
// Optional inline context construction block
|
|
305
|
+
let inlineContext = null;
|
|
306
|
+
if (this.check(token_1.TokenType.LParen)) {
|
|
307
|
+
inlineContext = this.parseInlineContext();
|
|
308
|
+
}
|
|
309
|
+
return { kind: 'WhenRule', token: tok, currentState, producedContext, nextState, narrative, inlineContext };
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Parses an inline context construction block:
|
|
313
|
+
* ( reason is "..." code is "..." )
|
|
314
|
+
*/
|
|
315
|
+
parseInlineContext() {
|
|
316
|
+
const tok = this.expect(token_1.TokenType.LParen);
|
|
317
|
+
const args = this.parseArguments();
|
|
318
|
+
this.expect(token_1.TokenType.RParen);
|
|
319
|
+
return { kind: 'InlineContext', token: tok, args };
|
|
320
|
+
}
|
|
321
|
+
// ── Implements ─────────────────────────────────────────────────────────────
|
|
322
|
+
/**
|
|
323
|
+
* Parses:
|
|
324
|
+
* implements Module.HandlerInterface (
|
|
325
|
+
* methodName param(Type) (
|
|
326
|
+
* if param is "/path"
|
|
327
|
+
* enter State "narrative"
|
|
328
|
+
* )
|
|
329
|
+
* )
|
|
330
|
+
*
|
|
331
|
+
* The method name (e.g. `switch`) is a plain camelCase identifier chosen
|
|
332
|
+
* by the designer on the handler interface — it is not a reserved keyword.
|
|
333
|
+
*/
|
|
334
|
+
parseImplements() {
|
|
335
|
+
const tok = this.expect(token_1.TokenType.Implements);
|
|
336
|
+
this.skipTrivia();
|
|
337
|
+
const interfaceName = this.parseQualifiedName();
|
|
338
|
+
this.skipTrivia();
|
|
339
|
+
this.expect(token_1.TokenType.LParen);
|
|
340
|
+
this.skipTrivia();
|
|
341
|
+
// handler method name param(Type) ( ... )
|
|
342
|
+
// The method name (e.g. `switch`) is a plain camelCase name chosen by the
|
|
343
|
+
// designer on the handler interface — it is not a reserved keyword.
|
|
344
|
+
// We consume the method name then the parameter name and its type.
|
|
345
|
+
this.expectIdent('handler method name'); // e.g. 'switch' — consumed but not stored
|
|
346
|
+
this.skipTrivia();
|
|
347
|
+
const switchParam = this.expectIdent('handler method parameter name');
|
|
348
|
+
this.skipTrivia();
|
|
349
|
+
this.expect(token_1.TokenType.LParen);
|
|
350
|
+
const switchParamType = this.parseType();
|
|
351
|
+
this.expect(token_1.TokenType.RParen);
|
|
352
|
+
this.skipTrivia();
|
|
353
|
+
this.expect(token_1.TokenType.LParen);
|
|
354
|
+
const branches = [];
|
|
355
|
+
while (!this.check(token_1.TokenType.RParen) && !this.check(token_1.TokenType.EOF)) {
|
|
356
|
+
this.skipTrivia();
|
|
357
|
+
if (this.check(token_1.TokenType.If)) {
|
|
358
|
+
branches.push(this.parseImplementsBranch());
|
|
359
|
+
}
|
|
360
|
+
else if (!this.check(token_1.TokenType.RParen)) {
|
|
361
|
+
this.advance();
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
this.expect(token_1.TokenType.RParen); // close method body
|
|
365
|
+
this.skipTrivia();
|
|
366
|
+
this.expect(token_1.TokenType.RParen); // close implements body
|
|
367
|
+
return { kind: 'ImplementsHandler', token: tok, interfaceName, switchParam, switchParamType, branches };
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Parses one branch inside an implements handler body:
|
|
371
|
+
* if param is "/path"
|
|
372
|
+
* enter State "narrative"
|
|
373
|
+
*/
|
|
374
|
+
parseImplementsBranch() {
|
|
375
|
+
const tok = this.expect(token_1.TokenType.If);
|
|
376
|
+
this.skipTrivia();
|
|
377
|
+
const condition = this.parseCondition();
|
|
378
|
+
this.skipTrivia();
|
|
379
|
+
this.expect(token_1.TokenType.Enter);
|
|
380
|
+
this.skipTrivia();
|
|
381
|
+
const targetState = this.expectIdent('target state name');
|
|
382
|
+
this.skipTrivia();
|
|
383
|
+
const narrative = this.parseOptionalString();
|
|
384
|
+
return { kind: 'ImplementsBranch', token: tok, condition, targetState, narrative };
|
|
385
|
+
}
|
|
386
|
+
// ── State ──────────────────────────────────────────────────────────────────
|
|
387
|
+
/**
|
|
388
|
+
* Parses:
|
|
389
|
+
* state StateName receives ?ContextName (
|
|
390
|
+
* returns ContextA, ContextB
|
|
391
|
+
* uses screen ScreenName
|
|
392
|
+
* )
|
|
393
|
+
*/
|
|
394
|
+
parseState() {
|
|
395
|
+
const tok = this.expect(token_1.TokenType.State);
|
|
396
|
+
this.skipTrivia();
|
|
397
|
+
const name = this.expectIdent('state name');
|
|
398
|
+
this.skipTrivia();
|
|
399
|
+
// Optional receives clause
|
|
400
|
+
let receives = null;
|
|
401
|
+
let receivesOptional = false;
|
|
402
|
+
if (this.check(token_1.TokenType.Receives)) {
|
|
403
|
+
this.advance();
|
|
404
|
+
this.skipTrivia();
|
|
405
|
+
if (this.check(token_1.TokenType.Question)) {
|
|
406
|
+
receivesOptional = true;
|
|
407
|
+
this.advance();
|
|
408
|
+
}
|
|
409
|
+
receives = this.expectIdent('context name in receives clause');
|
|
410
|
+
this.skipTrivia();
|
|
411
|
+
}
|
|
412
|
+
this.expect(token_1.TokenType.LParen);
|
|
413
|
+
let returns = null;
|
|
414
|
+
const uses = [];
|
|
415
|
+
while (!this.check(token_1.TokenType.RParen) && !this.check(token_1.TokenType.EOF)) {
|
|
416
|
+
this.skipTrivia();
|
|
417
|
+
if (this.check(token_1.TokenType.Returns)) {
|
|
418
|
+
returns = this.parseReturns();
|
|
419
|
+
}
|
|
420
|
+
else if (this.check(token_1.TokenType.Uses)) {
|
|
421
|
+
uses.push(...this.parseUsesBlock());
|
|
422
|
+
}
|
|
423
|
+
else if (!this.check(token_1.TokenType.RParen)) {
|
|
424
|
+
this.advance();
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
this.expect(token_1.TokenType.RParen);
|
|
428
|
+
return { kind: 'State', token: tok, module: '', name, receives, receivesOptional, returns, uses };
|
|
429
|
+
}
|
|
430
|
+
// ── Context ────────────────────────────────────────────────────────────────
|
|
431
|
+
/**
|
|
432
|
+
* Parses:
|
|
433
|
+
* context ContextName (
|
|
434
|
+
* field(Type),
|
|
435
|
+
* field(Type)
|
|
436
|
+
* )
|
|
437
|
+
*/
|
|
438
|
+
parseContext() {
|
|
439
|
+
const tok = this.expect(token_1.TokenType.Context);
|
|
440
|
+
this.skipTrivia();
|
|
441
|
+
const name = this.expectIdent('context name');
|
|
442
|
+
this.skipTrivia();
|
|
443
|
+
this.expect(token_1.TokenType.LParen);
|
|
444
|
+
const fields = this.parsePropList();
|
|
445
|
+
this.expect(token_1.TokenType.RParen);
|
|
446
|
+
return { kind: 'Context', token: tok, module: '', name, fields };
|
|
447
|
+
}
|
|
448
|
+
// ── Screen ─────────────────────────────────────────────────────────────────
|
|
449
|
+
/**
|
|
450
|
+
* Parses:
|
|
451
|
+
* screen ScreenName "description" (
|
|
452
|
+
* uses ( ... )
|
|
453
|
+
* )
|
|
454
|
+
*/
|
|
455
|
+
parseScreen() {
|
|
456
|
+
const tok = this.expect(token_1.TokenType.Screen);
|
|
457
|
+
this.skipTrivia();
|
|
458
|
+
const name = this.expectIdent('screen name');
|
|
459
|
+
this.skipTrivia();
|
|
460
|
+
const description = this.parseOptionalString();
|
|
461
|
+
this.skipTrivia();
|
|
462
|
+
this.expect(token_1.TokenType.LParen);
|
|
463
|
+
const uses = [];
|
|
464
|
+
while (!this.check(token_1.TokenType.RParen) && !this.check(token_1.TokenType.EOF)) {
|
|
465
|
+
this.skipTrivia();
|
|
466
|
+
if (this.check(token_1.TokenType.Uses)) {
|
|
467
|
+
uses.push(...this.parseUsesBlock());
|
|
468
|
+
}
|
|
469
|
+
else if (!this.check(token_1.TokenType.RParen)) {
|
|
470
|
+
this.advance();
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
this.expect(token_1.TokenType.RParen);
|
|
474
|
+
return { kind: 'Screen', token: tok, module: '', name, description, uses };
|
|
475
|
+
}
|
|
476
|
+
// ── View ───────────────────────────────────────────────────────────────────
|
|
477
|
+
/**
|
|
478
|
+
* Parses:
|
|
479
|
+
* view ViewName "description" (
|
|
480
|
+
* props ( ... )
|
|
481
|
+
* state ( ... )
|
|
482
|
+
* uses ( ... )
|
|
483
|
+
* )
|
|
484
|
+
*/
|
|
485
|
+
parseView() {
|
|
486
|
+
const tok = this.expect(token_1.TokenType.View);
|
|
487
|
+
this.skipTrivia();
|
|
488
|
+
const name = this.expectIdent('view name');
|
|
489
|
+
this.skipTrivia();
|
|
490
|
+
const description = this.parseOptionalString();
|
|
491
|
+
this.skipTrivia();
|
|
492
|
+
this.expect(token_1.TokenType.LParen);
|
|
493
|
+
let props = [];
|
|
494
|
+
let stateFields = [];
|
|
495
|
+
const uses = [];
|
|
496
|
+
while (!this.check(token_1.TokenType.RParen) && !this.check(token_1.TokenType.EOF)) {
|
|
497
|
+
this.skipTrivia();
|
|
498
|
+
if (this.check(token_1.TokenType.Props)) {
|
|
499
|
+
this.advance();
|
|
500
|
+
this.skipTrivia();
|
|
501
|
+
this.expect(token_1.TokenType.LParen);
|
|
502
|
+
props = this.parsePropList();
|
|
503
|
+
this.expect(token_1.TokenType.RParen);
|
|
504
|
+
}
|
|
505
|
+
else if (this.check(token_1.TokenType.State)) {
|
|
506
|
+
this.advance();
|
|
507
|
+
this.skipTrivia();
|
|
508
|
+
this.expect(token_1.TokenType.LParen);
|
|
509
|
+
stateFields = this.parsePropList();
|
|
510
|
+
this.expect(token_1.TokenType.RParen);
|
|
511
|
+
}
|
|
512
|
+
else if (this.check(token_1.TokenType.Uses)) {
|
|
513
|
+
uses.push(...this.parseUsesBlock());
|
|
514
|
+
}
|
|
515
|
+
else if (!this.check(token_1.TokenType.RParen)) {
|
|
516
|
+
this.advance();
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
this.expect(token_1.TokenType.RParen);
|
|
520
|
+
return { kind: 'View', token: tok, module: '', name, description, props, state: stateFields, uses };
|
|
521
|
+
}
|
|
522
|
+
// ── Provider ───────────────────────────────────────────────────────────────
|
|
523
|
+
/**
|
|
524
|
+
* Parses:
|
|
525
|
+
* provider ProviderName "description" (
|
|
526
|
+
* props ( ... )
|
|
527
|
+
* state ( ... )
|
|
528
|
+
* interface ( methods... )
|
|
529
|
+
* )
|
|
530
|
+
*/
|
|
531
|
+
parseProvider() {
|
|
532
|
+
const tok = this.expect(token_1.TokenType.Provider);
|
|
533
|
+
this.skipTrivia();
|
|
534
|
+
const name = this.expectIdent('provider name');
|
|
535
|
+
this.skipTrivia();
|
|
536
|
+
const description = this.parseOptionalString();
|
|
537
|
+
this.skipTrivia();
|
|
538
|
+
this.expect(token_1.TokenType.LParen);
|
|
539
|
+
let props = [];
|
|
540
|
+
let stateFields = [];
|
|
541
|
+
const methods = [];
|
|
542
|
+
while (!this.check(token_1.TokenType.RParen) && !this.check(token_1.TokenType.EOF)) {
|
|
543
|
+
this.skipTrivia();
|
|
544
|
+
if (this.check(token_1.TokenType.Props)) {
|
|
545
|
+
this.advance();
|
|
546
|
+
this.skipTrivia();
|
|
547
|
+
this.expect(token_1.TokenType.LParen);
|
|
548
|
+
props = this.parsePropList();
|
|
549
|
+
this.expect(token_1.TokenType.RParen);
|
|
550
|
+
}
|
|
551
|
+
else if (this.check(token_1.TokenType.State)) {
|
|
552
|
+
this.advance();
|
|
553
|
+
this.skipTrivia();
|
|
554
|
+
this.expect(token_1.TokenType.LParen);
|
|
555
|
+
stateFields = this.parsePropList();
|
|
556
|
+
this.expect(token_1.TokenType.RParen);
|
|
557
|
+
}
|
|
558
|
+
else if (this.check(token_1.TokenType.Interface)) {
|
|
559
|
+
this.advance();
|
|
560
|
+
this.skipTrivia();
|
|
561
|
+
this.expect(token_1.TokenType.LParen);
|
|
562
|
+
while (!this.check(token_1.TokenType.RParen) && !this.check(token_1.TokenType.EOF)) {
|
|
563
|
+
this.skipTrivia();
|
|
564
|
+
if (this.check(token_1.TokenType.CamelIdent)) {
|
|
565
|
+
methods.push(this.parseMethod());
|
|
566
|
+
}
|
|
567
|
+
else if (!this.check(token_1.TokenType.RParen)) {
|
|
568
|
+
this.advance();
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
this.expect(token_1.TokenType.RParen);
|
|
572
|
+
}
|
|
573
|
+
else if (!this.check(token_1.TokenType.RParen)) {
|
|
574
|
+
this.advance();
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
this.expect(token_1.TokenType.RParen);
|
|
578
|
+
return { kind: 'Provider', token: tok, module: '', name, description, props, state: stateFields, methods };
|
|
579
|
+
}
|
|
580
|
+
// ── Adapter ────────────────────────────────────────────────────────────────
|
|
581
|
+
/**
|
|
582
|
+
* Parses:
|
|
583
|
+
* adapter AdapterName "description" (
|
|
584
|
+
* props ( ... )
|
|
585
|
+
* state ( ... )
|
|
586
|
+
* interface ( methods... )
|
|
587
|
+
* )
|
|
588
|
+
*/
|
|
589
|
+
parseAdapter() {
|
|
590
|
+
const tok = this.expect(token_1.TokenType.Adapter);
|
|
591
|
+
this.skipTrivia();
|
|
592
|
+
const name = this.expectIdent('adapter name');
|
|
593
|
+
this.skipTrivia();
|
|
594
|
+
const description = this.parseOptionalString();
|
|
595
|
+
this.skipTrivia();
|
|
596
|
+
this.expect(token_1.TokenType.LParen);
|
|
597
|
+
let props = [];
|
|
598
|
+
let stateFields = [];
|
|
599
|
+
const methods = [];
|
|
600
|
+
while (!this.check(token_1.TokenType.RParen) && !this.check(token_1.TokenType.EOF)) {
|
|
601
|
+
this.skipTrivia();
|
|
602
|
+
if (this.check(token_1.TokenType.Props)) {
|
|
603
|
+
this.advance();
|
|
604
|
+
this.skipTrivia();
|
|
605
|
+
this.expect(token_1.TokenType.LParen);
|
|
606
|
+
props = this.parsePropList();
|
|
607
|
+
this.expect(token_1.TokenType.RParen);
|
|
608
|
+
}
|
|
609
|
+
else if (this.check(token_1.TokenType.State)) {
|
|
610
|
+
this.advance();
|
|
611
|
+
this.skipTrivia();
|
|
612
|
+
this.expect(token_1.TokenType.LParen);
|
|
613
|
+
stateFields = this.parsePropList();
|
|
614
|
+
this.expect(token_1.TokenType.RParen);
|
|
615
|
+
}
|
|
616
|
+
else if (this.check(token_1.TokenType.Interface)) {
|
|
617
|
+
this.advance();
|
|
618
|
+
this.skipTrivia();
|
|
619
|
+
this.expect(token_1.TokenType.LParen);
|
|
620
|
+
while (!this.check(token_1.TokenType.RParen) && !this.check(token_1.TokenType.EOF)) {
|
|
621
|
+
this.skipTrivia();
|
|
622
|
+
if (this.check(token_1.TokenType.CamelIdent)) {
|
|
623
|
+
methods.push(this.parseMethod());
|
|
624
|
+
}
|
|
625
|
+
else if (!this.check(token_1.TokenType.RParen)) {
|
|
626
|
+
this.advance();
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
this.expect(token_1.TokenType.RParen);
|
|
630
|
+
}
|
|
631
|
+
else if (!this.check(token_1.TokenType.RParen)) {
|
|
632
|
+
this.advance();
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
this.expect(token_1.TokenType.RParen);
|
|
636
|
+
return { kind: 'Adapter', token: tok, module: '', name, description, props, state: stateFields, methods };
|
|
637
|
+
}
|
|
638
|
+
// ── Interface ──────────────────────────────────────────────────────────────
|
|
639
|
+
/**
|
|
640
|
+
* Parses:
|
|
641
|
+
* interface InterfaceName "description" (
|
|
642
|
+
* props ( ... )
|
|
643
|
+
* state ( ... )
|
|
644
|
+
* uses ( ... )
|
|
645
|
+
* methodName param(Type) returns(Type) "description"
|
|
646
|
+
* ...
|
|
647
|
+
* )
|
|
648
|
+
*
|
|
649
|
+
* When used as a module-level handler interface, the body contains a
|
|
650
|
+
* method declaration whose body is a series of `if` branches. The method
|
|
651
|
+
* name (e.g. `switch`) is a plain camelCase identifier — not a keyword.
|
|
652
|
+
*/
|
|
653
|
+
parseInterface() {
|
|
654
|
+
const tok = this.expect(token_1.TokenType.Interface);
|
|
655
|
+
this.skipTrivia();
|
|
656
|
+
const name = this.expectIdent('interface name');
|
|
657
|
+
this.skipTrivia();
|
|
658
|
+
const description = this.parseOptionalString();
|
|
659
|
+
this.skipTrivia();
|
|
660
|
+
this.expect(token_1.TokenType.LParen);
|
|
661
|
+
let props = [];
|
|
662
|
+
let stateFields = [];
|
|
663
|
+
const methods = [];
|
|
664
|
+
const uses = [];
|
|
665
|
+
while (!this.check(token_1.TokenType.RParen) && !this.check(token_1.TokenType.EOF)) {
|
|
666
|
+
this.skipTrivia();
|
|
667
|
+
if (this.check(token_1.TokenType.Props)) {
|
|
668
|
+
this.advance();
|
|
669
|
+
this.skipTrivia();
|
|
670
|
+
this.expect(token_1.TokenType.LParen);
|
|
671
|
+
props = this.parsePropList();
|
|
672
|
+
this.expect(token_1.TokenType.RParen);
|
|
673
|
+
}
|
|
674
|
+
else if (this.check(token_1.TokenType.State)) {
|
|
675
|
+
this.advance();
|
|
676
|
+
this.skipTrivia();
|
|
677
|
+
this.expect(token_1.TokenType.LParen);
|
|
678
|
+
stateFields = this.parsePropList();
|
|
679
|
+
this.expect(token_1.TokenType.RParen);
|
|
680
|
+
}
|
|
681
|
+
else if (this.check(token_1.TokenType.Uses)) {
|
|
682
|
+
uses.push(...this.parseUsesBlock());
|
|
683
|
+
}
|
|
684
|
+
else if (this.check(token_1.TokenType.CamelIdent)) {
|
|
685
|
+
// Methods appear directly in the body after props.
|
|
686
|
+
// This includes handler interface method declarations whose body
|
|
687
|
+
// contains if-branches (e.g. `switch path(string) ( if ... )`).
|
|
688
|
+
methods.push(this.parseInterfaceMethod());
|
|
689
|
+
}
|
|
690
|
+
else if (!this.check(token_1.TokenType.RParen)) {
|
|
691
|
+
this.advance();
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
this.expect(token_1.TokenType.RParen);
|
|
695
|
+
return { kind: 'Interface', token: tok, module: '', name, description, props, state: stateFields, methods, uses };
|
|
696
|
+
}
|
|
697
|
+
// ── Returns clause ─────────────────────────────────────────────────────────
|
|
698
|
+
/**
|
|
699
|
+
* Parses either the simple or expanded form of a returns clause.
|
|
700
|
+
*
|
|
701
|
+
* Simple: returns ContextA, ContextB
|
|
702
|
+
* Expanded: returns ( ContextA ( sideEffects ) ContextB ( sideEffects ) )
|
|
703
|
+
*/
|
|
704
|
+
parseReturns() {
|
|
705
|
+
const tok = this.expect(token_1.TokenType.Returns);
|
|
706
|
+
this.skipTrivia();
|
|
707
|
+
// Expanded form starts with '('
|
|
708
|
+
if (this.check(token_1.TokenType.LParen)) {
|
|
709
|
+
this.advance();
|
|
710
|
+
const entries = [];
|
|
711
|
+
while (!this.check(token_1.TokenType.RParen) && !this.check(token_1.TokenType.EOF)) {
|
|
712
|
+
this.skipTrivia();
|
|
713
|
+
if (this.check(token_1.TokenType.PascalIdent)) {
|
|
714
|
+
const ctxTok = this.current();
|
|
715
|
+
const contextName = this.advance().value;
|
|
716
|
+
this.skipTrivia();
|
|
717
|
+
const sideEffects = [];
|
|
718
|
+
if (this.check(token_1.TokenType.LParen)) {
|
|
719
|
+
this.advance();
|
|
720
|
+
while (!this.check(token_1.TokenType.RParen) && !this.check(token_1.TokenType.EOF)) {
|
|
721
|
+
this.skipTrivia();
|
|
722
|
+
if (this.checkSystemCall()) {
|
|
723
|
+
const callTok = this.current();
|
|
724
|
+
const call = this.parseQualifiedName();
|
|
725
|
+
this.skipTrivia();
|
|
726
|
+
const args = this.parseInlineArgList();
|
|
727
|
+
sideEffects.push({ kind: 'SideEffect', token: callTok, call, args });
|
|
728
|
+
}
|
|
729
|
+
else if (!this.check(token_1.TokenType.RParen)) {
|
|
730
|
+
this.advance();
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
this.expect(token_1.TokenType.RParen);
|
|
734
|
+
}
|
|
735
|
+
entries.push({ kind: 'ExpandedReturn', token: ctxTok, contextName, sideEffects });
|
|
736
|
+
}
|
|
737
|
+
else if (!this.check(token_1.TokenType.RParen)) {
|
|
738
|
+
this.advance();
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
this.expect(token_1.TokenType.RParen);
|
|
742
|
+
return { kind: 'ExpandedReturns', token: tok, entries };
|
|
743
|
+
}
|
|
744
|
+
// Simple form — comma-separated list of context names
|
|
745
|
+
const contexts = [];
|
|
746
|
+
if (this.check(token_1.TokenType.PascalIdent)) {
|
|
747
|
+
contexts.push(this.advance().value);
|
|
748
|
+
while (this.check(token_1.TokenType.Comma)) {
|
|
749
|
+
this.advance();
|
|
750
|
+
this.skipTrivia();
|
|
751
|
+
if (this.check(token_1.TokenType.PascalIdent)) {
|
|
752
|
+
contexts.push(this.advance().value);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
return { kind: 'SimpleReturns', token: tok, contexts };
|
|
757
|
+
}
|
|
758
|
+
// ── Uses block ─────────────────────────────────────────────────────────────
|
|
759
|
+
/**
|
|
760
|
+
* Parses a `uses` block and returns its entries.
|
|
761
|
+
* Handles both the single-entry form and the parenthesised list form.
|
|
762
|
+
*
|
|
763
|
+
* Single: `uses screen LoginScreen`
|
|
764
|
+
* List: `uses ( view A, view B, if ... )`
|
|
765
|
+
*/
|
|
766
|
+
parseUsesBlock() {
|
|
767
|
+
this.expect(token_1.TokenType.Uses);
|
|
768
|
+
this.skipTrivia();
|
|
769
|
+
if (this.check(token_1.TokenType.LParen)) {
|
|
770
|
+
this.advance();
|
|
771
|
+
const entries = this.parseUseEntries();
|
|
772
|
+
this.expect(token_1.TokenType.RParen);
|
|
773
|
+
return entries;
|
|
774
|
+
}
|
|
775
|
+
// Single entry without parentheses
|
|
776
|
+
const entry = this.parseUseEntry();
|
|
777
|
+
return entry ? [entry] : [];
|
|
778
|
+
}
|
|
779
|
+
/**
|
|
780
|
+
* Parses a comma-separated sequence of use entries inside a `uses ( ... )` block.
|
|
781
|
+
*/
|
|
782
|
+
parseUseEntries() {
|
|
783
|
+
const entries = [];
|
|
784
|
+
let lastPos = -1;
|
|
785
|
+
while (!this.check(token_1.TokenType.RParen) && !this.check(token_1.TokenType.EOF)) {
|
|
786
|
+
if (this.pos === lastPos) {
|
|
787
|
+
this.advance();
|
|
788
|
+
continue;
|
|
789
|
+
}
|
|
790
|
+
lastPos = this.pos;
|
|
791
|
+
this.skipTrivia();
|
|
792
|
+
if (this.check(token_1.TokenType.RParen))
|
|
793
|
+
break;
|
|
794
|
+
const entry = this.parseUseEntry();
|
|
795
|
+
if (entry)
|
|
796
|
+
entries.push(entry);
|
|
797
|
+
this.skipTrivia();
|
|
798
|
+
if (this.check(token_1.TokenType.Comma))
|
|
799
|
+
this.advance();
|
|
800
|
+
}
|
|
801
|
+
return entries;
|
|
802
|
+
}
|
|
803
|
+
/**
|
|
804
|
+
* Parses a single use entry — a component use, a conditional block, or
|
|
805
|
+
* an iteration block.
|
|
806
|
+
*/
|
|
807
|
+
parseUseEntry() {
|
|
808
|
+
this.skipTrivia();
|
|
809
|
+
const tok = this.current();
|
|
810
|
+
if (this.check(token_1.TokenType.If))
|
|
811
|
+
return this.parseConditionalBlock();
|
|
812
|
+
if (this.check(token_1.TokenType.For))
|
|
813
|
+
return this.parseIterationBlock();
|
|
814
|
+
// Component kinds: screen, view, adapter, provider, interface
|
|
815
|
+
if (this.check(token_1.TokenType.Screen) ||
|
|
816
|
+
this.check(token_1.TokenType.View) ||
|
|
817
|
+
this.check(token_1.TokenType.Adapter) ||
|
|
818
|
+
this.check(token_1.TokenType.Provider) ||
|
|
819
|
+
this.check(token_1.TokenType.Interface)) {
|
|
820
|
+
return this.parseComponentUse();
|
|
821
|
+
}
|
|
822
|
+
this.error(diagnostics_1.DiagnosticCode.P_INVALID_CONSTRUCT_POSITION, `Unexpected token '${tok.value}' inside uses block`, tok);
|
|
823
|
+
this.advance();
|
|
824
|
+
return null;
|
|
825
|
+
}
|
|
826
|
+
/**
|
|
827
|
+
* Parses a single component use:
|
|
828
|
+
* view UIModule.LoginForm ( args... uses... )
|
|
829
|
+
* adapter SessionAdapter.checkSession
|
|
830
|
+
* screen LoginScreen
|
|
831
|
+
*/
|
|
832
|
+
parseComponentUse() {
|
|
833
|
+
const tok = this.current();
|
|
834
|
+
const componentKind = tok.value;
|
|
835
|
+
this.advance();
|
|
836
|
+
this.skipTrivia();
|
|
837
|
+
const name = this.parseQualifiedName();
|
|
838
|
+
this.skipTrivia();
|
|
839
|
+
const args = [];
|
|
840
|
+
const uses = [];
|
|
841
|
+
// Arguments and nested uses can appear either inline or in a paren block
|
|
842
|
+
if (this.check(token_1.TokenType.LParen)) {
|
|
843
|
+
this.advance();
|
|
844
|
+
// Inside the paren block: arguments and nested use entries
|
|
845
|
+
while (!this.check(token_1.TokenType.RParen) && !this.check(token_1.TokenType.EOF)) {
|
|
846
|
+
this.skipTrivia();
|
|
847
|
+
if (this.check(token_1.TokenType.Uses)) {
|
|
848
|
+
uses.push(...this.parseUsesBlock());
|
|
849
|
+
}
|
|
850
|
+
else if (this.checkArgumentStart()) {
|
|
851
|
+
args.push(...this.parseArguments());
|
|
852
|
+
}
|
|
853
|
+
else if (this.isUseEntry()) {
|
|
854
|
+
const entry = this.parseUseEntry();
|
|
855
|
+
if (entry)
|
|
856
|
+
uses.push(entry);
|
|
857
|
+
}
|
|
858
|
+
else if (!this.check(token_1.TokenType.RParen)) {
|
|
859
|
+
this.advance();
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
this.expect(token_1.TokenType.RParen);
|
|
863
|
+
}
|
|
864
|
+
else {
|
|
865
|
+
// Inline arguments without parens: view X key is value, key is value
|
|
866
|
+
if (this.checkArgumentStart()) {
|
|
867
|
+
args.push(...this.parseInlineArgList());
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
return { kind: 'ComponentUse', token: tok, componentKind, name, args, uses };
|
|
871
|
+
}
|
|
872
|
+
/**
|
|
873
|
+
* Parses:
|
|
874
|
+
* if condition ( useEntries... )
|
|
875
|
+
*/
|
|
876
|
+
parseConditionalBlock() {
|
|
877
|
+
const tok = this.expect(token_1.TokenType.If);
|
|
878
|
+
this.skipTrivia();
|
|
879
|
+
const condition = this.parseCondition();
|
|
880
|
+
this.skipTrivia();
|
|
881
|
+
this.expect(token_1.TokenType.LParen);
|
|
882
|
+
const body = this.parseUseEntries();
|
|
883
|
+
this.expect(token_1.TokenType.RParen);
|
|
884
|
+
return { kind: 'ConditionalBlock', token: tok, condition, body };
|
|
885
|
+
}
|
|
886
|
+
/**
|
|
887
|
+
* Parses:
|
|
888
|
+
* for collection as binding ( useEntries... )
|
|
889
|
+
* for collection as key, value ( useEntries... )
|
|
890
|
+
*/
|
|
891
|
+
parseIterationBlock() {
|
|
892
|
+
const tok = this.expect(token_1.TokenType.For);
|
|
893
|
+
this.skipTrivia();
|
|
894
|
+
const collection = this.parseAccessExpression();
|
|
895
|
+
this.skipTrivia();
|
|
896
|
+
this.expect(token_1.TokenType.As);
|
|
897
|
+
this.skipTrivia();
|
|
898
|
+
const bindings = [];
|
|
899
|
+
bindings.push(this.expectIdent('iteration binding'));
|
|
900
|
+
this.skipTrivia();
|
|
901
|
+
// Map iteration: two bindings separated by comma
|
|
902
|
+
if (this.check(token_1.TokenType.Comma)) {
|
|
903
|
+
this.advance();
|
|
904
|
+
this.skipTrivia();
|
|
905
|
+
bindings.push(this.expectIdent('second iteration binding'));
|
|
906
|
+
this.skipTrivia();
|
|
907
|
+
}
|
|
908
|
+
this.expect(token_1.TokenType.LParen);
|
|
909
|
+
const body = this.parseUseEntries();
|
|
910
|
+
this.expect(token_1.TokenType.RParen);
|
|
911
|
+
return { kind: 'IterationBlock', token: tok, collection, bindings, body };
|
|
912
|
+
}
|
|
913
|
+
// ── Condition ──────────────────────────────────────────────────────────────
|
|
914
|
+
/**
|
|
915
|
+
* Parses a condition expression:
|
|
916
|
+
* state.context is AccountDeauthenticated
|
|
917
|
+
* state.context.status is "pending"
|
|
918
|
+
* state.context is not AccountRecovered
|
|
919
|
+
*/
|
|
920
|
+
parseCondition() {
|
|
921
|
+
const tok = this.current();
|
|
922
|
+
const left = this.parseAccessExpression();
|
|
923
|
+
this.skipTrivia();
|
|
924
|
+
let operator = 'is';
|
|
925
|
+
if (this.check(token_1.TokenType.IsNot)) {
|
|
926
|
+
operator = 'is not';
|
|
927
|
+
this.advance();
|
|
928
|
+
}
|
|
929
|
+
else if (this.check(token_1.TokenType.Is)) {
|
|
930
|
+
this.advance();
|
|
931
|
+
}
|
|
932
|
+
else {
|
|
933
|
+
this.error(diagnostics_1.DiagnosticCode.P_MISSING_IS, `Expected 'is' or 'is not' in condition`, this.current());
|
|
934
|
+
}
|
|
935
|
+
this.skipTrivia();
|
|
936
|
+
const right = this.parseExpression();
|
|
937
|
+
return { kind: 'Condition', token: tok, left, operator, right };
|
|
938
|
+
}
|
|
939
|
+
// ── Arguments ──────────────────────────────────────────────────────────────
|
|
940
|
+
/**
|
|
941
|
+
* Parses a comma-separated list of `name is value` arguments.
|
|
942
|
+
* Used inside parenthesised component use bodies and system calls.
|
|
943
|
+
*/
|
|
944
|
+
parseArguments() {
|
|
945
|
+
const args = [];
|
|
946
|
+
let lastPos = -1;
|
|
947
|
+
while (this.checkArgumentStart()) {
|
|
948
|
+
if (this.pos === lastPos)
|
|
949
|
+
break;
|
|
950
|
+
lastPos = this.pos;
|
|
951
|
+
args.push(this.parseArgument());
|
|
952
|
+
this.skipTrivia();
|
|
953
|
+
if (this.check(token_1.TokenType.Comma))
|
|
954
|
+
this.advance();
|
|
955
|
+
this.skipTrivia();
|
|
956
|
+
}
|
|
957
|
+
return args;
|
|
958
|
+
}
|
|
959
|
+
/**
|
|
960
|
+
* Parses a comma-separated list of inline `name is value` arguments
|
|
961
|
+
* without an enclosing paren block. Stops at newline, `)`, or EOF.
|
|
962
|
+
*/
|
|
963
|
+
parseInlineArgList() {
|
|
964
|
+
const args = [];
|
|
965
|
+
while (this.checkArgumentStart()) {
|
|
966
|
+
args.push(this.parseArgument());
|
|
967
|
+
this.skipTrivia();
|
|
968
|
+
if (this.check(token_1.TokenType.Comma)) {
|
|
969
|
+
this.advance();
|
|
970
|
+
this.skipTrivia();
|
|
971
|
+
}
|
|
972
|
+
else {
|
|
973
|
+
break;
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
return args;
|
|
977
|
+
}
|
|
978
|
+
/**
|
|
979
|
+
* Parses a single argument: `name is value`
|
|
980
|
+
*/
|
|
981
|
+
parseArgument() {
|
|
982
|
+
const tok = this.current();
|
|
983
|
+
const name = this.expectIdent('argument name');
|
|
984
|
+
this.skipTrivia();
|
|
985
|
+
if (!this.check(token_1.TokenType.Is)) {
|
|
986
|
+
this.error(diagnostics_1.DiagnosticCode.P_MISSING_IS, `Expected 'is' after argument name '${name}'`, this.current());
|
|
987
|
+
}
|
|
988
|
+
else {
|
|
989
|
+
this.advance();
|
|
990
|
+
}
|
|
991
|
+
this.skipTrivia();
|
|
992
|
+
const value = this.parseExpression();
|
|
993
|
+
return { kind: 'Argument', token: tok, name, value };
|
|
994
|
+
}
|
|
995
|
+
// ── Expressions ────────────────────────────────────────────────────────────
|
|
996
|
+
/**
|
|
997
|
+
* Parses an expression — a value on the right-hand side of an `is` assignment
|
|
998
|
+
* or a condition operand.
|
|
999
|
+
*
|
|
1000
|
+
* Handles:
|
|
1001
|
+
* - Block expressions: `( state.return(x) )`
|
|
1002
|
+
* - state.return(): `state.return(contextName)`
|
|
1003
|
+
* - Access expressions: `state.context.fullName`, `props.items`
|
|
1004
|
+
* - Call expressions: `system.getContext(SystemUser)`
|
|
1005
|
+
* - Literals: `"string"`, `42`, `3.14`, `true`, `false`, `[]`, `{}`
|
|
1006
|
+
* - PascalCase type references: `SystemUser` (e.g. in system.getContext(SystemUser))
|
|
1007
|
+
*/
|
|
1008
|
+
parseExpression() {
|
|
1009
|
+
const tok = this.current();
|
|
1010
|
+
// Block expression: ( ... )
|
|
1011
|
+
if (this.check(token_1.TokenType.LParen)) {
|
|
1012
|
+
return this.parseBlockExpression();
|
|
1013
|
+
}
|
|
1014
|
+
// Literals
|
|
1015
|
+
if (this.check(token_1.TokenType.StringLit))
|
|
1016
|
+
return this.parseStringLiteral();
|
|
1017
|
+
if (this.check(token_1.TokenType.IntegerLit))
|
|
1018
|
+
return this.parseIntegerLiteral();
|
|
1019
|
+
if (this.check(token_1.TokenType.FloatLit))
|
|
1020
|
+
return this.parseFloatLiteral();
|
|
1021
|
+
if (this.check(token_1.TokenType.BooleanLit))
|
|
1022
|
+
return this.parseBooleanLiteral();
|
|
1023
|
+
if (this.checkListLiteral())
|
|
1024
|
+
return this.parseListLiteral();
|
|
1025
|
+
if (this.checkMapLiteral())
|
|
1026
|
+
return this.parseMapLiteral();
|
|
1027
|
+
// PascalCase identifier — type reference as argument value
|
|
1028
|
+
// e.g. system.getContext(SystemUser)
|
|
1029
|
+
if (this.check(token_1.TokenType.PascalIdent)) {
|
|
1030
|
+
return {
|
|
1031
|
+
kind: 'AccessExpression',
|
|
1032
|
+
token: tok,
|
|
1033
|
+
path: [this.advance().value],
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
// Access expression or call expression starting with a camelCase name,
|
|
1037
|
+
// 'state', or 'system'
|
|
1038
|
+
if (this.check(token_1.TokenType.CamelIdent) ||
|
|
1039
|
+
this.check(token_1.TokenType.State) ||
|
|
1040
|
+
this.check(token_1.TokenType.System)) {
|
|
1041
|
+
return this.parseAccessOrCall();
|
|
1042
|
+
}
|
|
1043
|
+
// Fallback — emit error and return a dummy access expression
|
|
1044
|
+
this.error(diagnostics_1.DiagnosticCode.P_UNEXPECTED_TOKEN, `Unexpected token '${tok.value}' in expression`, tok);
|
|
1045
|
+
this.advance();
|
|
1046
|
+
return { kind: 'AccessExpression', token: tok, path: ['?'] };
|
|
1047
|
+
}
|
|
1048
|
+
/**
|
|
1049
|
+
* Parses a block expression: `( statements... )`
|
|
1050
|
+
* The body contains either state.return() calls or assignment statements.
|
|
1051
|
+
*/
|
|
1052
|
+
parseBlockExpression() {
|
|
1053
|
+
const tok = this.expect(token_1.TokenType.LParen);
|
|
1054
|
+
const statements = [];
|
|
1055
|
+
while (!this.check(token_1.TokenType.RParen) && !this.check(token_1.TokenType.EOF)) {
|
|
1056
|
+
this.skipTrivia();
|
|
1057
|
+
if (this.check(token_1.TokenType.RParen))
|
|
1058
|
+
break;
|
|
1059
|
+
const stmt = this.parseStatement();
|
|
1060
|
+
if (stmt)
|
|
1061
|
+
statements.push(stmt);
|
|
1062
|
+
this.skipTrivia();
|
|
1063
|
+
}
|
|
1064
|
+
this.expect(token_1.TokenType.RParen);
|
|
1065
|
+
return { kind: 'BlockExpression', token: tok, statements };
|
|
1066
|
+
}
|
|
1067
|
+
/**
|
|
1068
|
+
* Parses a statement inside a block expression body.
|
|
1069
|
+
*
|
|
1070
|
+
* `state.return(contextName)` → StateReturnStatement
|
|
1071
|
+
* `state.fieldName is value` → AssignmentStatement
|
|
1072
|
+
*/
|
|
1073
|
+
parseStatement() {
|
|
1074
|
+
const tok = this.current();
|
|
1075
|
+
if (this.check(token_1.TokenType.State)) {
|
|
1076
|
+
this.advance(); // consume 'state'
|
|
1077
|
+
this.skipTrivia();
|
|
1078
|
+
this.expect(token_1.TokenType.Dot);
|
|
1079
|
+
this.skipTrivia();
|
|
1080
|
+
// state.return(contextName)
|
|
1081
|
+
if (this.check(token_1.TokenType.CamelIdent) && this.current().value === 'return') {
|
|
1082
|
+
this.advance(); // consume 'return'
|
|
1083
|
+
this.expect(token_1.TokenType.LParen);
|
|
1084
|
+
this.skipTrivia();
|
|
1085
|
+
const contextName = this.expectIdent('context name in state.return()');
|
|
1086
|
+
this.skipTrivia();
|
|
1087
|
+
this.expect(token_1.TokenType.RParen);
|
|
1088
|
+
return { kind: 'StateReturnStatement', token: tok, contextName };
|
|
1089
|
+
}
|
|
1090
|
+
// state.fieldName is value — assignment
|
|
1091
|
+
const fieldName = this.expectIdent('state field name');
|
|
1092
|
+
this.skipTrivia();
|
|
1093
|
+
this.expect(token_1.TokenType.Is);
|
|
1094
|
+
this.skipTrivia();
|
|
1095
|
+
const value = this.parseExpression();
|
|
1096
|
+
const target = { kind: 'AccessExpression', token: tok, path: ['state', fieldName] };
|
|
1097
|
+
return { kind: 'AssignmentStatement', token: tok, target, value };
|
|
1098
|
+
}
|
|
1099
|
+
// Unknown statement — skip
|
|
1100
|
+
this.error(diagnostics_1.DiagnosticCode.P_UNEXPECTED_TOKEN, `Unexpected token '${tok.value}' in block`, tok);
|
|
1101
|
+
this.advance();
|
|
1102
|
+
return null;
|
|
1103
|
+
}
|
|
1104
|
+
/**
|
|
1105
|
+
* Parses an access expression or call expression starting with a camelCase
|
|
1106
|
+
* name, 'state', or 'system'.
|
|
1107
|
+
*
|
|
1108
|
+
* If the path ends with `(...)`, it becomes a CallExpression.
|
|
1109
|
+
* If the path ends with `return(...)`, it becomes a StateReturnExpression.
|
|
1110
|
+
*/
|
|
1111
|
+
parseAccessOrCall() {
|
|
1112
|
+
const tok = this.current();
|
|
1113
|
+
const path = [];
|
|
1114
|
+
// Consume the first segment
|
|
1115
|
+
path.push(this.advance().value);
|
|
1116
|
+
// Follow dot-separated segments
|
|
1117
|
+
while (this.check(token_1.TokenType.Dot)) {
|
|
1118
|
+
this.advance(); // consume '.'
|
|
1119
|
+
if (this.check(token_1.TokenType.CamelIdent) ||
|
|
1120
|
+
this.check(token_1.TokenType.PascalIdent) ||
|
|
1121
|
+
this.check(token_1.TokenType.State) ||
|
|
1122
|
+
this.check(token_1.TokenType.System) ||
|
|
1123
|
+
this.check(token_1.TokenType.Context) ||
|
|
1124
|
+
this.check(token_1.TokenType.Returns)) {
|
|
1125
|
+
path.push(this.advance().value);
|
|
1126
|
+
}
|
|
1127
|
+
else {
|
|
1128
|
+
break;
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
// state.return(contextName) — special form
|
|
1132
|
+
if (path[0] === 'state' && path[1] === 'return' && this.check(token_1.TokenType.LParen)) {
|
|
1133
|
+
this.advance(); // consume '('
|
|
1134
|
+
this.skipTrivia();
|
|
1135
|
+
const contextName = this.expectIdent('context name in state.return()');
|
|
1136
|
+
this.skipTrivia();
|
|
1137
|
+
this.expect(token_1.TokenType.RParen);
|
|
1138
|
+
return { kind: 'StateReturnExpression', token: tok, contextName };
|
|
1139
|
+
}
|
|
1140
|
+
// Call expression: path(args...)
|
|
1141
|
+
// Supports both keyword-style args (`name is value`) and a single positional
|
|
1142
|
+
// PascalIdent type reference (e.g. `system.getContext(SystemUser)`).
|
|
1143
|
+
if (this.check(token_1.TokenType.LParen)) {
|
|
1144
|
+
this.advance(); // consume '('
|
|
1145
|
+
const args = this.parseArguments();
|
|
1146
|
+
if (args.length === 0 && this.check(token_1.TokenType.PascalIdent)) {
|
|
1147
|
+
const valTok = this.current();
|
|
1148
|
+
const valName = this.advance().value;
|
|
1149
|
+
const value = { kind: 'AccessExpression', token: valTok, path: [valName] };
|
|
1150
|
+
args.push({ kind: 'Argument', token: valTok, name: '', value });
|
|
1151
|
+
}
|
|
1152
|
+
this.expect(token_1.TokenType.RParen);
|
|
1153
|
+
const callee = { kind: 'AccessExpression', token: tok, path };
|
|
1154
|
+
return { kind: 'CallExpression', token: tok, callee, args };
|
|
1155
|
+
}
|
|
1156
|
+
// Plain access expression
|
|
1157
|
+
return { kind: 'AccessExpression', token: tok, path };
|
|
1158
|
+
}
|
|
1159
|
+
/**
|
|
1160
|
+
* Parses a dot-separated access path — always returns an AccessExpressionNode.
|
|
1161
|
+
* Used in conditions and iteration blocks where a call is not expected.
|
|
1162
|
+
*/
|
|
1163
|
+
parseAccessExpression() {
|
|
1164
|
+
const tok = this.current();
|
|
1165
|
+
const path = [];
|
|
1166
|
+
path.push(this.advance().value);
|
|
1167
|
+
while (this.check(token_1.TokenType.Dot)) {
|
|
1168
|
+
this.advance();
|
|
1169
|
+
if (this.check(token_1.TokenType.CamelIdent) ||
|
|
1170
|
+
this.check(token_1.TokenType.PascalIdent) ||
|
|
1171
|
+
this.check(token_1.TokenType.State) ||
|
|
1172
|
+
this.check(token_1.TokenType.System) ||
|
|
1173
|
+
this.check(token_1.TokenType.Context)) {
|
|
1174
|
+
path.push(this.advance().value);
|
|
1175
|
+
}
|
|
1176
|
+
else {
|
|
1177
|
+
break;
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
return { kind: 'AccessExpression', token: tok, path };
|
|
1181
|
+
}
|
|
1182
|
+
// ── Literals ───────────────────────────────────────────────────────────────
|
|
1183
|
+
parseStringLiteral() {
|
|
1184
|
+
const tok = this.advance();
|
|
1185
|
+
// Strip surrounding quotes from the raw token value
|
|
1186
|
+
const value = tok.value.slice(1, -1);
|
|
1187
|
+
return { kind: 'StringLiteral', token: tok, value };
|
|
1188
|
+
}
|
|
1189
|
+
parseIntegerLiteral() {
|
|
1190
|
+
const tok = this.advance();
|
|
1191
|
+
return { kind: 'IntegerLiteral', token: tok, value: parseInt(tok.value, 10) };
|
|
1192
|
+
}
|
|
1193
|
+
parseFloatLiteral() {
|
|
1194
|
+
const tok = this.advance();
|
|
1195
|
+
return { kind: 'FloatLiteral', token: tok, value: parseFloat(tok.value) };
|
|
1196
|
+
}
|
|
1197
|
+
parseBooleanLiteral() {
|
|
1198
|
+
const tok = this.advance();
|
|
1199
|
+
return { kind: 'BooleanLiteral', token: tok, value: tok.value === 'true' };
|
|
1200
|
+
}
|
|
1201
|
+
parseListLiteral() {
|
|
1202
|
+
const tok = this.current();
|
|
1203
|
+
this.advance(); // '['
|
|
1204
|
+
this.advance(); // ']'
|
|
1205
|
+
return { kind: 'ListLiteral', token: tok, elements: [] };
|
|
1206
|
+
}
|
|
1207
|
+
parseMapLiteral() {
|
|
1208
|
+
const tok = this.current();
|
|
1209
|
+
this.advance(); // '{'
|
|
1210
|
+
this.advance(); // '}'
|
|
1211
|
+
return { kind: 'MapLiteral', token: tok, entries: [] };
|
|
1212
|
+
}
|
|
1213
|
+
// ── Props ──────────────────────────────────────────────────────────────────
|
|
1214
|
+
/**
|
|
1215
|
+
* Parses a comma-separated list of prop declarations inside a `props` or
|
|
1216
|
+
* `state` or `context` block. Stops at `)` or EOF.
|
|
1217
|
+
*
|
|
1218
|
+
* Forms:
|
|
1219
|
+
* `name(Type)` — typed data prop
|
|
1220
|
+
* `name(Type) is default` — typed prop with default value
|
|
1221
|
+
* `name argName(Type)` — interaction prop with argument variable
|
|
1222
|
+
* `name` — interaction prop with no payload
|
|
1223
|
+
* `?name(Type)` — optional typed prop (in context fields)
|
|
1224
|
+
*/
|
|
1225
|
+
parsePropList() {
|
|
1226
|
+
const props = [];
|
|
1227
|
+
while (!this.check(token_1.TokenType.RParen) && !this.check(token_1.TokenType.EOF)) {
|
|
1228
|
+
this.skipTrivia();
|
|
1229
|
+
if (this.check(token_1.TokenType.RParen))
|
|
1230
|
+
break;
|
|
1231
|
+
const tok = this.current();
|
|
1232
|
+
let optional = false;
|
|
1233
|
+
if (this.check(token_1.TokenType.Question)) {
|
|
1234
|
+
optional = true;
|
|
1235
|
+
this.advance();
|
|
1236
|
+
}
|
|
1237
|
+
if (!this.check(token_1.TokenType.CamelIdent))
|
|
1238
|
+
break;
|
|
1239
|
+
const name = this.advance().value;
|
|
1240
|
+
this.skipTrivia();
|
|
1241
|
+
let type = null;
|
|
1242
|
+
let argName = null;
|
|
1243
|
+
let defaultValue = null;
|
|
1244
|
+
if (this.check(token_1.TokenType.LParen)) {
|
|
1245
|
+
// name(Type) — direct type annotation
|
|
1246
|
+
this.advance();
|
|
1247
|
+
type = this.parseType();
|
|
1248
|
+
this.expect(token_1.TokenType.RParen);
|
|
1249
|
+
optional = optional || (type.kind === 'NamedType' && type.optional);
|
|
1250
|
+
}
|
|
1251
|
+
else if (this.check(token_1.TokenType.CamelIdent)) {
|
|
1252
|
+
// name argName(Type) — interaction prop with argument variable
|
|
1253
|
+
argName = this.advance().value;
|
|
1254
|
+
this.skipTrivia();
|
|
1255
|
+
if (this.check(token_1.TokenType.LParen)) {
|
|
1256
|
+
this.advance();
|
|
1257
|
+
type = this.parseType();
|
|
1258
|
+
this.expect(token_1.TokenType.RParen);
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
this.skipTrivia();
|
|
1262
|
+
if (this.check(token_1.TokenType.Is)) {
|
|
1263
|
+
this.advance();
|
|
1264
|
+
this.skipTrivia();
|
|
1265
|
+
defaultValue = this.parseLiteralValue();
|
|
1266
|
+
}
|
|
1267
|
+
props.push({ kind: 'Prop', token: tok, name, type, optional, defaultValue, argName });
|
|
1268
|
+
this.skipTrivia();
|
|
1269
|
+
if (this.check(token_1.TokenType.Comma))
|
|
1270
|
+
this.advance();
|
|
1271
|
+
}
|
|
1272
|
+
return props;
|
|
1273
|
+
}
|
|
1274
|
+
// ── Types ──────────────────────────────────────────────────────────────────
|
|
1275
|
+
/**
|
|
1276
|
+
* Parses a type annotation. Called when the cursor is on the type keyword or name.
|
|
1277
|
+
*/
|
|
1278
|
+
parseType() {
|
|
1279
|
+
const tok = this.current();
|
|
1280
|
+
if (this.check(token_1.TokenType.TList)) {
|
|
1281
|
+
this.advance();
|
|
1282
|
+
this.expect(token_1.TokenType.LParen);
|
|
1283
|
+
const elementType = this.parseType();
|
|
1284
|
+
this.expect(token_1.TokenType.RParen);
|
|
1285
|
+
return { kind: 'ListType', token: tok, elementType };
|
|
1286
|
+
}
|
|
1287
|
+
if (this.check(token_1.TokenType.TMap)) {
|
|
1288
|
+
this.advance();
|
|
1289
|
+
this.expect(token_1.TokenType.LParen);
|
|
1290
|
+
const keyType = this.parseType();
|
|
1291
|
+
this.expect(token_1.TokenType.Comma);
|
|
1292
|
+
this.skipTrivia();
|
|
1293
|
+
const valueType = this.parseType();
|
|
1294
|
+
this.expect(token_1.TokenType.RParen);
|
|
1295
|
+
return { kind: 'MapType', token: tok, keyType, valueType };
|
|
1296
|
+
}
|
|
1297
|
+
// Primitive types
|
|
1298
|
+
// Note: the lexer maps the word 'context' to TokenType.Context (the construct
|
|
1299
|
+
// keyword token), never to TContext. We accept both here so 'context' works
|
|
1300
|
+
// as a type in system interface methods (e.g. returns(context)).
|
|
1301
|
+
const primitiveMap = {
|
|
1302
|
+
[token_1.TokenType.TString]: 'string',
|
|
1303
|
+
[token_1.TokenType.TInteger]: 'integer',
|
|
1304
|
+
[token_1.TokenType.TFloat]: 'float',
|
|
1305
|
+
[token_1.TokenType.TBoolean]: 'boolean',
|
|
1306
|
+
[token_1.TokenType.TContext]: 'context',
|
|
1307
|
+
[token_1.TokenType.Context]: 'context',
|
|
1308
|
+
};
|
|
1309
|
+
if (tok.type in primitiveMap) {
|
|
1310
|
+
this.advance();
|
|
1311
|
+
return { kind: 'PrimitiveType', token: tok, name: primitiveMap[tok.type] };
|
|
1312
|
+
}
|
|
1313
|
+
// Optional named type: ?Product
|
|
1314
|
+
let optional = false;
|
|
1315
|
+
if (this.check(token_1.TokenType.Question)) {
|
|
1316
|
+
optional = true;
|
|
1317
|
+
this.advance();
|
|
1318
|
+
}
|
|
1319
|
+
if (this.check(token_1.TokenType.PascalIdent)) {
|
|
1320
|
+
const name = this.advance().value;
|
|
1321
|
+
return { kind: 'NamedType', token: tok, name, optional };
|
|
1322
|
+
}
|
|
1323
|
+
this.error(diagnostics_1.DiagnosticCode.P_MISSING_TYPE, `Expected a type`, tok);
|
|
1324
|
+
return { kind: 'NamedType', token: tok, name: '?', optional: false };
|
|
1325
|
+
}
|
|
1326
|
+
// ── Methods ────────────────────────────────────────────────────────────────
|
|
1327
|
+
/**
|
|
1328
|
+
* Parses a standard method declaration in a provider, adapter, or interface body.
|
|
1329
|
+
* Delegates to `parseInterfaceMethod` which also handles handler method bodies.
|
|
1330
|
+
*/
|
|
1331
|
+
parseMethod() {
|
|
1332
|
+
return this.parseInterfaceMethod();
|
|
1333
|
+
}
|
|
1334
|
+
/**
|
|
1335
|
+
* Parses a method declaration that may optionally have a body block of
|
|
1336
|
+
* `if` branches — used for handler interface method declarations.
|
|
1337
|
+
*
|
|
1338
|
+
* Forms:
|
|
1339
|
+
* `methodName "description"`
|
|
1340
|
+
* `methodName param(Type) returns(Type) "description"`
|
|
1341
|
+
* `methodName param(Type) ( if param is "/path" enter State "narrative" )`
|
|
1342
|
+
*/
|
|
1343
|
+
parseInterfaceMethod() {
|
|
1344
|
+
const tok = this.current();
|
|
1345
|
+
const name = this.advance().value; // camelIdent
|
|
1346
|
+
this.skipTrivia();
|
|
1347
|
+
const params = [];
|
|
1348
|
+
let returnType = null;
|
|
1349
|
+
let description = null;
|
|
1350
|
+
// Parse parameters: paramName(Type) ...
|
|
1351
|
+
while (this.check(token_1.TokenType.CamelIdent)) {
|
|
1352
|
+
const paramTok = this.current();
|
|
1353
|
+
const paramName = this.advance().value;
|
|
1354
|
+
this.skipTrivia();
|
|
1355
|
+
let paramType = null;
|
|
1356
|
+
if (this.check(token_1.TokenType.LParen)) {
|
|
1357
|
+
this.advance();
|
|
1358
|
+
paramType = this.parseType();
|
|
1359
|
+
this.expect(token_1.TokenType.RParen);
|
|
1360
|
+
}
|
|
1361
|
+
params.push({ kind: 'Prop', token: paramTok, name: paramName, type: paramType, optional: false, defaultValue: null, argName: null });
|
|
1362
|
+
this.skipTrivia();
|
|
1363
|
+
}
|
|
1364
|
+
// Handler interface method body: ( if ... enter ... )
|
|
1365
|
+
// Consume and discard — the body is structural metadata, not executable logic.
|
|
1366
|
+
if (this.check(token_1.TokenType.LParen)) {
|
|
1367
|
+
this.advance();
|
|
1368
|
+
let depth = 1;
|
|
1369
|
+
while (!this.check(token_1.TokenType.EOF) && depth > 0) {
|
|
1370
|
+
if (this.check(token_1.TokenType.LParen))
|
|
1371
|
+
depth++;
|
|
1372
|
+
else if (this.check(token_1.TokenType.RParen))
|
|
1373
|
+
depth--;
|
|
1374
|
+
if (depth > 0)
|
|
1375
|
+
this.advance();
|
|
1376
|
+
}
|
|
1377
|
+
this.expect(token_1.TokenType.RParen);
|
|
1378
|
+
return { kind: 'Method', token: tok, name, params, returnType: null, description: null };
|
|
1379
|
+
}
|
|
1380
|
+
// Optional returns(Type)
|
|
1381
|
+
if (this.check(token_1.TokenType.Returns)) {
|
|
1382
|
+
this.advance();
|
|
1383
|
+
this.expect(token_1.TokenType.LParen);
|
|
1384
|
+
returnType = this.parseType();
|
|
1385
|
+
this.expect(token_1.TokenType.RParen);
|
|
1386
|
+
this.skipTrivia();
|
|
1387
|
+
}
|
|
1388
|
+
// Optional description string
|
|
1389
|
+
if (this.check(token_1.TokenType.StringLit)) {
|
|
1390
|
+
description = this.advance().value.slice(1, -1);
|
|
1391
|
+
}
|
|
1392
|
+
return { kind: 'Method', token: tok, name, params, returnType, description };
|
|
1393
|
+
}
|
|
1394
|
+
// ── Qualified name ─────────────────────────────────────────────────────────
|
|
1395
|
+
/**
|
|
1396
|
+
* Parses a dot-separated qualified name.
|
|
1397
|
+
* e.g. `UIModule.LoginForm`, `system.setContext`, `SessionAdapter.checkSession`
|
|
1398
|
+
*/
|
|
1399
|
+
parseQualifiedName() {
|
|
1400
|
+
const tok = this.current();
|
|
1401
|
+
const parts = [];
|
|
1402
|
+
if (this.check(token_1.TokenType.PascalIdent) ||
|
|
1403
|
+
this.check(token_1.TokenType.CamelIdent) ||
|
|
1404
|
+
this.check(token_1.TokenType.System) ||
|
|
1405
|
+
this.check(token_1.TokenType.State)) {
|
|
1406
|
+
parts.push(this.advance().value);
|
|
1407
|
+
}
|
|
1408
|
+
while (this.check(token_1.TokenType.Dot)) {
|
|
1409
|
+
this.advance();
|
|
1410
|
+
if (this.check(token_1.TokenType.PascalIdent) ||
|
|
1411
|
+
this.check(token_1.TokenType.CamelIdent) ||
|
|
1412
|
+
this.check(token_1.TokenType.State) ||
|
|
1413
|
+
this.check(token_1.TokenType.System)) {
|
|
1414
|
+
parts.push(this.advance().value);
|
|
1415
|
+
}
|
|
1416
|
+
else {
|
|
1417
|
+
break;
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
return { kind: 'QualifiedName', token: tok, parts };
|
|
1421
|
+
}
|
|
1422
|
+
// ── Literal value (for defaults) ───────────────────────────────────────────
|
|
1423
|
+
/**
|
|
1424
|
+
* Parses a literal value used as a default in a prop or state declaration.
|
|
1425
|
+
* Handles strings, integers, floats, booleans, `[]`, and `{}`.
|
|
1426
|
+
*/
|
|
1427
|
+
parseLiteralValue() {
|
|
1428
|
+
const tok = this.current();
|
|
1429
|
+
if (this.check(token_1.TokenType.StringLit))
|
|
1430
|
+
return this.parseStringLiteral();
|
|
1431
|
+
if (this.check(token_1.TokenType.IntegerLit))
|
|
1432
|
+
return this.parseIntegerLiteral();
|
|
1433
|
+
if (this.check(token_1.TokenType.FloatLit))
|
|
1434
|
+
return this.parseFloatLiteral();
|
|
1435
|
+
if (this.check(token_1.TokenType.BooleanLit))
|
|
1436
|
+
return this.parseBooleanLiteral();
|
|
1437
|
+
if (this.checkListLiteral())
|
|
1438
|
+
return this.parseListLiteral();
|
|
1439
|
+
if (this.checkMapLiteral())
|
|
1440
|
+
return this.parseMapLiteral();
|
|
1441
|
+
this.error(diagnostics_1.DiagnosticCode.P_UNEXPECTED_TOKEN, `Expected a literal value`, tok);
|
|
1442
|
+
this.advance();
|
|
1443
|
+
return { kind: 'StringLiteral', token: tok, value: '' };
|
|
1444
|
+
}
|
|
1445
|
+
// ── Token stream helpers ───────────────────────────────────────────────────
|
|
1446
|
+
/** Returns the token at the current position. */
|
|
1447
|
+
current() {
|
|
1448
|
+
return this.tokens[this.pos] ?? this.tokens[this.tokens.length - 1];
|
|
1449
|
+
}
|
|
1450
|
+
/** Returns true if the current token has the given type. */
|
|
1451
|
+
check(type) {
|
|
1452
|
+
return this.current().type === type;
|
|
1453
|
+
}
|
|
1454
|
+
/**
|
|
1455
|
+
* Consumes and returns the current token, advancing the position.
|
|
1456
|
+
*/
|
|
1457
|
+
advance() {
|
|
1458
|
+
const tok = this.current();
|
|
1459
|
+
if (tok.type !== token_1.TokenType.EOF)
|
|
1460
|
+
this.pos++;
|
|
1461
|
+
return tok;
|
|
1462
|
+
}
|
|
1463
|
+
/**
|
|
1464
|
+
* Consumes the current token if it matches `type` and returns it.
|
|
1465
|
+
* If it does not match, emits a diagnostic and returns null without advancing.
|
|
1466
|
+
*/
|
|
1467
|
+
expect(type) {
|
|
1468
|
+
if (this.check(type))
|
|
1469
|
+
return this.advance();
|
|
1470
|
+
const tok = this.current();
|
|
1471
|
+
this.error(diagnostics_1.DiagnosticCode.P_UNEXPECTED_TOKEN, `Expected '${type}' but found '${tok.value}'`, tok);
|
|
1472
|
+
return null;
|
|
1473
|
+
}
|
|
1474
|
+
/**
|
|
1475
|
+
* Consumes a PascalIdent or CamelIdent and returns its value.
|
|
1476
|
+
* Emits a diagnostic if neither is present.
|
|
1477
|
+
*/
|
|
1478
|
+
expectIdent(context) {
|
|
1479
|
+
if (this.check(token_1.TokenType.PascalIdent) || this.check(token_1.TokenType.CamelIdent)) {
|
|
1480
|
+
return this.advance().value;
|
|
1481
|
+
}
|
|
1482
|
+
const tok = this.current();
|
|
1483
|
+
this.error(diagnostics_1.DiagnosticCode.P_MISSING_IDENTIFIER, `Expected identifier (${context}) but found '${tok.value}'`, tok);
|
|
1484
|
+
return '?';
|
|
1485
|
+
}
|
|
1486
|
+
/**
|
|
1487
|
+
* Consumes and returns the string value if the current token is a StringLit.
|
|
1488
|
+
* Returns null without advancing if it is not.
|
|
1489
|
+
*/
|
|
1490
|
+
parseOptionalString() {
|
|
1491
|
+
if (this.check(token_1.TokenType.StringLit)) {
|
|
1492
|
+
const val = this.advance().value;
|
|
1493
|
+
return val.slice(1, -1);
|
|
1494
|
+
}
|
|
1495
|
+
return null;
|
|
1496
|
+
}
|
|
1497
|
+
/**
|
|
1498
|
+
* Skips comment tokens only.
|
|
1499
|
+
*/
|
|
1500
|
+
skipComments() {
|
|
1501
|
+
while (this.check(token_1.TokenType.Comment))
|
|
1502
|
+
this.advance();
|
|
1503
|
+
}
|
|
1504
|
+
/**
|
|
1505
|
+
* Skips comments and newlines — the whitespace between meaningful tokens.
|
|
1506
|
+
*/
|
|
1507
|
+
skipTrivia() {
|
|
1508
|
+
while (this.check(token_1.TokenType.Comment) || this.check(token_1.TokenType.Newline))
|
|
1509
|
+
this.advance();
|
|
1510
|
+
}
|
|
1511
|
+
/**
|
|
1512
|
+
* Returns true if the current token looks like a system.* call.
|
|
1513
|
+
*/
|
|
1514
|
+
checkSystemCall() {
|
|
1515
|
+
return this.check(token_1.TokenType.System);
|
|
1516
|
+
}
|
|
1517
|
+
/**
|
|
1518
|
+
* Returns true if the current token can start an argument (`name is value`).
|
|
1519
|
+
* Arguments start with a camelCase identifier.
|
|
1520
|
+
*/
|
|
1521
|
+
checkArgumentStart() {
|
|
1522
|
+
return this.check(token_1.TokenType.CamelIdent);
|
|
1523
|
+
}
|
|
1524
|
+
/**
|
|
1525
|
+
* Returns true if the current token can start a use entry (component kind,
|
|
1526
|
+
* if, or for).
|
|
1527
|
+
*/
|
|
1528
|
+
isUseEntry() {
|
|
1529
|
+
return (this.check(token_1.TokenType.View) ||
|
|
1530
|
+
this.check(token_1.TokenType.Screen) ||
|
|
1531
|
+
this.check(token_1.TokenType.Adapter) ||
|
|
1532
|
+
this.check(token_1.TokenType.Provider) ||
|
|
1533
|
+
this.check(token_1.TokenType.Interface) ||
|
|
1534
|
+
this.check(token_1.TokenType.If) ||
|
|
1535
|
+
this.check(token_1.TokenType.For));
|
|
1536
|
+
}
|
|
1537
|
+
/**
|
|
1538
|
+
* Returns true if the current two tokens form a `[` `]` list literal.
|
|
1539
|
+
* The lexer does not produce bracket tokens — `[]` is detected by checking
|
|
1540
|
+
* for an Unknown token with value `[` followed by one with value `]`.
|
|
1541
|
+
*/
|
|
1542
|
+
checkListLiteral() {
|
|
1543
|
+
return (this.current().type === token_1.TokenType.Unknown &&
|
|
1544
|
+
this.current().value === '[' &&
|
|
1545
|
+
this.tokens[this.pos + 1]?.value === ']');
|
|
1546
|
+
}
|
|
1547
|
+
/**
|
|
1548
|
+
* Returns true if the current two tokens form a `{` `}` map literal.
|
|
1549
|
+
*/
|
|
1550
|
+
checkMapLiteral() {
|
|
1551
|
+
return (this.current().type === token_1.TokenType.Unknown &&
|
|
1552
|
+
this.current().value === '{' &&
|
|
1553
|
+
this.tokens[this.pos + 1]?.value === '}');
|
|
1554
|
+
}
|
|
1555
|
+
/**
|
|
1556
|
+
* Parses a system.* call expression appearing at the statement level
|
|
1557
|
+
* (e.g. inside a module body or a uses block).
|
|
1558
|
+
*/
|
|
1559
|
+
parseSystemCall() {
|
|
1560
|
+
const tok = this.current();
|
|
1561
|
+
const callee = this.parseAccessExpression();
|
|
1562
|
+
this.skipTrivia();
|
|
1563
|
+
const args = this.parseInlineArgList();
|
|
1564
|
+
return { kind: 'CallExpression', token: tok, callee, args };
|
|
1565
|
+
}
|
|
1566
|
+
// ── Error recovery ─────────────────────────────────────────────────────────
|
|
1567
|
+
/**
|
|
1568
|
+
* Emits a diagnostic at the given token's position.
|
|
1569
|
+
*/
|
|
1570
|
+
error(code, message, tok) {
|
|
1571
|
+
const range = (0, diagnostics_1.rangeFromToken)(tok.line, tok.column, tok.value.length || 1);
|
|
1572
|
+
this.diagnostics.push((0, diagnostics_1.parseDiagnostic)(code, message, 'error', range));
|
|
1573
|
+
}
|
|
1574
|
+
/**
|
|
1575
|
+
* Advances past tokens until a safe synchronisation point is found.
|
|
1576
|
+
* Used after an unrecoverable parse error to resume at the next construct.
|
|
1577
|
+
* Synchronisation points: top-level keyword, closing paren, or EOF.
|
|
1578
|
+
*/
|
|
1579
|
+
synchronise() {
|
|
1580
|
+
while (!this.check(token_1.TokenType.EOF)) {
|
|
1581
|
+
switch (this.current().type) {
|
|
1582
|
+
case token_1.TokenType.System:
|
|
1583
|
+
case token_1.TokenType.Module:
|
|
1584
|
+
case token_1.TokenType.State:
|
|
1585
|
+
case token_1.TokenType.Context:
|
|
1586
|
+
case token_1.TokenType.Screen:
|
|
1587
|
+
case token_1.TokenType.View:
|
|
1588
|
+
case token_1.TokenType.Provider:
|
|
1589
|
+
case token_1.TokenType.Adapter:
|
|
1590
|
+
case token_1.TokenType.Interface:
|
|
1591
|
+
case token_1.TokenType.RParen:
|
|
1592
|
+
return;
|
|
1593
|
+
default:
|
|
1594
|
+
this.advance();
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
exports.Parser = Parser;
|
|
1600
|
+
//# sourceMappingURL=parser.js.map
|