@orcalang/orca-runtime-ts 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/bus.d.ts +46 -0
- package/dist/bus.d.ts.map +1 -0
- package/dist/bus.js +148 -0
- package/dist/bus.js.map +1 -0
- package/dist/effects.d.ts +13 -0
- package/dist/effects.d.ts.map +1 -0
- package/dist/effects.js +33 -0
- package/dist/effects.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/logging.d.ts +46 -0
- package/dist/logging.d.ts.map +1 -0
- package/dist/logging.js +59 -0
- package/dist/logging.js.map +1 -0
- package/dist/machine.d.ts +119 -0
- package/dist/machine.d.ts.map +1 -0
- package/dist/machine.js +785 -0
- package/dist/machine.js.map +1 -0
- package/dist/parser.d.ts +25 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +737 -0
- package/dist/parser.js.map +1 -0
- package/dist/persistence.d.ts +20 -0
- package/dist/persistence.d.ts.map +1 -0
- package/dist/persistence.js +34 -0
- package/dist/persistence.js.map +1 -0
- package/dist/types.d.ts +138 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +95 -0
- package/dist/types.js.map +1 -0
- package/package.json +36 -0
package/dist/parser.js
ADDED
|
@@ -0,0 +1,737 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orca markdown parser.
|
|
3
|
+
*
|
|
4
|
+
* Parses .orca.md format into MachineDef objects.
|
|
5
|
+
* Supports hierarchical (nested) states and parallel regions.
|
|
6
|
+
*/
|
|
7
|
+
export class ParseError extends Error {
|
|
8
|
+
constructor(message) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "ParseError";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function parseMarkdownStructure(source) {
|
|
14
|
+
const lines = source.split('\n');
|
|
15
|
+
const elements = [];
|
|
16
|
+
let i = 0;
|
|
17
|
+
while (i < lines.length) {
|
|
18
|
+
const trimmed = lines[i].trim();
|
|
19
|
+
if (trimmed === '') {
|
|
20
|
+
i++;
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
// Skip fenced code blocks
|
|
24
|
+
if (trimmed.startsWith('```')) {
|
|
25
|
+
i++;
|
|
26
|
+
while (i < lines.length && !lines[i].trim().startsWith('```'))
|
|
27
|
+
i++;
|
|
28
|
+
if (i < lines.length)
|
|
29
|
+
i++;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
// Horizontal rule separator
|
|
33
|
+
if (trimmed === '---') {
|
|
34
|
+
elements.push({ kind: 'separator' });
|
|
35
|
+
i++;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
// Heading
|
|
39
|
+
const headingMatch = trimmed.match(/^(#{1,6})\s+(.+)$/);
|
|
40
|
+
if (headingMatch) {
|
|
41
|
+
elements.push({ kind: 'heading', level: headingMatch[1].length, text: headingMatch[2].trim() });
|
|
42
|
+
i++;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
// Blockquote
|
|
46
|
+
if (trimmed.startsWith('>')) {
|
|
47
|
+
const quoteLines = [];
|
|
48
|
+
while (i < lines.length && lines[i].trim().startsWith('>')) {
|
|
49
|
+
quoteLines.push(lines[i].trim().replace(/^>\s*/, ''));
|
|
50
|
+
i++;
|
|
51
|
+
}
|
|
52
|
+
elements.push({ kind: 'blockquote', text: quoteLines.join('\n') });
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
// Table
|
|
56
|
+
if (trimmed.startsWith('|')) {
|
|
57
|
+
const tableLines = [];
|
|
58
|
+
while (i < lines.length && lines[i].trim().startsWith('|')) {
|
|
59
|
+
tableLines.push(lines[i].trim());
|
|
60
|
+
i++;
|
|
61
|
+
}
|
|
62
|
+
if (tableLines.length >= 2) {
|
|
63
|
+
const headers = splitTableRow(tableLines[0]);
|
|
64
|
+
const isSeparator = /^\|[\s\-:|]+\|$/.test(tableLines[1]);
|
|
65
|
+
const dataStart = isSeparator ? 2 : 1;
|
|
66
|
+
const rows = [];
|
|
67
|
+
for (let j = dataStart; j < tableLines.length; j++) {
|
|
68
|
+
rows.push(splitTableRow(tableLines[j]));
|
|
69
|
+
}
|
|
70
|
+
elements.push({ kind: 'table', headers, rows });
|
|
71
|
+
}
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
// Bullet list
|
|
75
|
+
if (trimmed.startsWith('- ')) {
|
|
76
|
+
const items = [];
|
|
77
|
+
while (i < lines.length && lines[i].trim().startsWith('- ')) {
|
|
78
|
+
items.push(lines[i].trim().substring(2).trim());
|
|
79
|
+
i++;
|
|
80
|
+
}
|
|
81
|
+
elements.push({ kind: 'bullets', items });
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
// Skip other text
|
|
85
|
+
i++;
|
|
86
|
+
}
|
|
87
|
+
return elements;
|
|
88
|
+
}
|
|
89
|
+
function splitTableRow(line) {
|
|
90
|
+
const cells = line.split('|').map(c => c.trim());
|
|
91
|
+
if (cells.length > 0 && cells[0] === '')
|
|
92
|
+
cells.shift();
|
|
93
|
+
if (cells.length > 0 && cells[cells.length - 1] === '')
|
|
94
|
+
cells.pop();
|
|
95
|
+
return cells;
|
|
96
|
+
}
|
|
97
|
+
function stripBackticks(text) {
|
|
98
|
+
if (text.startsWith('`') && text.endsWith('`'))
|
|
99
|
+
return text.slice(1, -1);
|
|
100
|
+
return text;
|
|
101
|
+
}
|
|
102
|
+
function findColumnIndex(headers, name) {
|
|
103
|
+
return headers.findIndex(h => h.toLowerCase() === name.toLowerCase());
|
|
104
|
+
}
|
|
105
|
+
function parseMdAnnotations(text) {
|
|
106
|
+
let isInitial = false, isFinal = false, isParallel = false;
|
|
107
|
+
let syncStrategy;
|
|
108
|
+
const bracketMatch = text.match(/\[(.+)\]/);
|
|
109
|
+
if (bracketMatch) {
|
|
110
|
+
for (const part of bracketMatch[1].split(',').map(p => p.trim())) {
|
|
111
|
+
if (part === 'initial')
|
|
112
|
+
isInitial = true;
|
|
113
|
+
else if (part === 'final')
|
|
114
|
+
isFinal = true;
|
|
115
|
+
else if (part === 'parallel')
|
|
116
|
+
isParallel = true;
|
|
117
|
+
else if (part.startsWith('sync:')) {
|
|
118
|
+
const v = part.slice(5).trim();
|
|
119
|
+
if (v === 'all-final' || v === 'all_final')
|
|
120
|
+
syncStrategy = 'all-final';
|
|
121
|
+
else if (v === 'any-final' || v === 'any_final')
|
|
122
|
+
syncStrategy = 'any-final';
|
|
123
|
+
else if (v === 'custom')
|
|
124
|
+
syncStrategy = 'custom';
|
|
125
|
+
}
|
|
126
|
+
else if (part.includes('sync:')) {
|
|
127
|
+
// Handle "parallel sync: any_final" format (no comma between annotations)
|
|
128
|
+
if (part.includes('parallel'))
|
|
129
|
+
isParallel = true;
|
|
130
|
+
const v = part.split('sync:')[1]?.trim();
|
|
131
|
+
if (v === 'all-final' || v === 'all_final')
|
|
132
|
+
syncStrategy = 'all-final';
|
|
133
|
+
else if (v === 'any-final' || v === 'any_final')
|
|
134
|
+
syncStrategy = 'any-final';
|
|
135
|
+
else if (v === 'custom')
|
|
136
|
+
syncStrategy = 'custom';
|
|
137
|
+
}
|
|
138
|
+
else if (part.includes('parallel')) {
|
|
139
|
+
// Handle "parallel" when combined with other non-sync annotations
|
|
140
|
+
isParallel = true;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return { isInitial, isFinal, isParallel, syncStrategy };
|
|
145
|
+
}
|
|
146
|
+
function parseMdStateBullet(entry, text) {
|
|
147
|
+
if (text.startsWith('on_entry:')) {
|
|
148
|
+
let val = text.slice(9).trim();
|
|
149
|
+
if (val.startsWith('->'))
|
|
150
|
+
val = val.slice(2).trim();
|
|
151
|
+
entry.onEntry = val;
|
|
152
|
+
}
|
|
153
|
+
else if (text.startsWith('on_exit:')) {
|
|
154
|
+
let val = text.slice(8).trim();
|
|
155
|
+
if (val.startsWith('->'))
|
|
156
|
+
val = val.slice(2).trim();
|
|
157
|
+
entry.onExit = val;
|
|
158
|
+
}
|
|
159
|
+
else if (text.startsWith('timeout:')) {
|
|
160
|
+
const rest = text.slice(8).trim();
|
|
161
|
+
const arrowIdx = rest.indexOf('->');
|
|
162
|
+
if (arrowIdx !== -1) {
|
|
163
|
+
entry.timeout = { duration: rest.slice(0, arrowIdx).trim(), target: rest.slice(arrowIdx + 2).trim() };
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
else if (text.startsWith('ignore:')) {
|
|
167
|
+
const names = text.slice(7).trim().split(',').map(e => e.trim()).filter(Boolean);
|
|
168
|
+
if (!entry.ignoredEvents)
|
|
169
|
+
entry.ignoredEvents = [];
|
|
170
|
+
entry.ignoredEvents.push(...names);
|
|
171
|
+
}
|
|
172
|
+
else if (text.startsWith('on_done:')) {
|
|
173
|
+
let val = text.slice(8).trim();
|
|
174
|
+
if (val.startsWith('->'))
|
|
175
|
+
val = val.slice(2).trim();
|
|
176
|
+
if (entry.invoke) {
|
|
177
|
+
entry.invoke.onDone = val;
|
|
178
|
+
}
|
|
179
|
+
entry.onDone = val;
|
|
180
|
+
}
|
|
181
|
+
else if (text.startsWith('on_error:')) {
|
|
182
|
+
let val = text.slice(9).trim();
|
|
183
|
+
if (val.startsWith('->'))
|
|
184
|
+
val = val.slice(2).trim();
|
|
185
|
+
if (entry.invoke) {
|
|
186
|
+
entry.invoke.onError = val;
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
entry._pendingOnError = val;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
else if (text.startsWith('invoke:')) {
|
|
193
|
+
const rest = text.slice(7).trim();
|
|
194
|
+
// Format: "MachineName" or "MachineName input: { field: ctx.field }"
|
|
195
|
+
const inputMatch = rest.match(/^(\w+)\s+input:\s*\{(.+)\}$/);
|
|
196
|
+
const pendingOnError = entry._pendingOnError;
|
|
197
|
+
delete entry._pendingOnError;
|
|
198
|
+
if (inputMatch) {
|
|
199
|
+
const machineName = inputMatch[1];
|
|
200
|
+
const inputStr = inputMatch[2];
|
|
201
|
+
const input = {};
|
|
202
|
+
const pairs = inputStr.split(',').map(p => p.trim());
|
|
203
|
+
for (const pair of pairs) {
|
|
204
|
+
const colonIdx = pair.indexOf(':');
|
|
205
|
+
if (colonIdx !== -1) {
|
|
206
|
+
const key = pair.slice(0, colonIdx).trim();
|
|
207
|
+
const val = pair.slice(colonIdx + 1).trim();
|
|
208
|
+
input[key] = val;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
entry.invoke = { machine: machineName, input, onError: pendingOnError };
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
entry.invoke = { machine: rest, onError: pendingOnError };
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
function buildMdStatesAtLevel(entries, startIdx, level, parentName) {
|
|
219
|
+
const states = [];
|
|
220
|
+
let i = startIdx;
|
|
221
|
+
while (i < entries.length) {
|
|
222
|
+
const entry = entries[i];
|
|
223
|
+
if (entry.level < level)
|
|
224
|
+
break;
|
|
225
|
+
if (entry.type === 'region')
|
|
226
|
+
break;
|
|
227
|
+
if (entry.level > level) {
|
|
228
|
+
i++;
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
const state = {
|
|
232
|
+
name: entry.name,
|
|
233
|
+
isInitial: entry.isInitial,
|
|
234
|
+
isFinal: entry.isFinal,
|
|
235
|
+
contains: [],
|
|
236
|
+
ignoredEvents: entry.ignoredEvents || [],
|
|
237
|
+
};
|
|
238
|
+
if (parentName)
|
|
239
|
+
state.parent = parentName;
|
|
240
|
+
if (entry.description)
|
|
241
|
+
state.description = entry.description;
|
|
242
|
+
if (entry.onEntry)
|
|
243
|
+
state.onEntry = entry.onEntry;
|
|
244
|
+
if (entry.onExit)
|
|
245
|
+
state.onExit = entry.onExit;
|
|
246
|
+
if (entry.onDone)
|
|
247
|
+
state.onDone = entry.onDone;
|
|
248
|
+
if (entry.timeout)
|
|
249
|
+
state.timeout = entry.timeout;
|
|
250
|
+
if (entry.invoke)
|
|
251
|
+
state.invoke = entry.invoke;
|
|
252
|
+
i++;
|
|
253
|
+
if (entry.isParallel) {
|
|
254
|
+
const result = buildMdParallelRegions(entries, i, level + 1, entry.name, entry.syncStrategy);
|
|
255
|
+
state.parallel = result.parallelDef;
|
|
256
|
+
i = result.nextIdx;
|
|
257
|
+
}
|
|
258
|
+
else if (i < entries.length && entries[i].level === level + 1 && entries[i].type === 'state') {
|
|
259
|
+
const result = buildMdStatesAtLevel(entries, i, level + 1, entry.name);
|
|
260
|
+
state.contains = result.states;
|
|
261
|
+
i = result.nextIdx;
|
|
262
|
+
}
|
|
263
|
+
states.push(state);
|
|
264
|
+
}
|
|
265
|
+
return { states, nextIdx: i };
|
|
266
|
+
}
|
|
267
|
+
function buildMdParallelRegions(entries, startIdx, regionLevel, parentName, syncStrategy) {
|
|
268
|
+
const regions = [];
|
|
269
|
+
let i = startIdx;
|
|
270
|
+
while (i < entries.length && entries[i].level >= regionLevel) {
|
|
271
|
+
if (entries[i].type !== 'region' || entries[i].level !== regionLevel)
|
|
272
|
+
break;
|
|
273
|
+
const regionName = entries[i].name;
|
|
274
|
+
i++;
|
|
275
|
+
const regionStates = [];
|
|
276
|
+
while (i < entries.length && entries[i].level > regionLevel) {
|
|
277
|
+
if (entries[i].type === 'state' && entries[i].level === regionLevel + 1) {
|
|
278
|
+
const e = entries[i];
|
|
279
|
+
const s = {
|
|
280
|
+
name: e.name,
|
|
281
|
+
isInitial: e.isInitial,
|
|
282
|
+
isFinal: e.isFinal,
|
|
283
|
+
parent: `${parentName}.${regionName}`,
|
|
284
|
+
contains: [],
|
|
285
|
+
ignoredEvents: e.ignoredEvents || [],
|
|
286
|
+
};
|
|
287
|
+
if (e.description)
|
|
288
|
+
s.description = e.description;
|
|
289
|
+
if (e.onEntry)
|
|
290
|
+
s.onEntry = e.onEntry;
|
|
291
|
+
if (e.onExit)
|
|
292
|
+
s.onExit = e.onExit;
|
|
293
|
+
if (e.timeout)
|
|
294
|
+
s.timeout = e.timeout;
|
|
295
|
+
regionStates.push(s);
|
|
296
|
+
i++;
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
regions.push({ name: regionName, states: regionStates });
|
|
303
|
+
}
|
|
304
|
+
return { parallelDef: { regions, sync: syncStrategy }, nextIdx: i };
|
|
305
|
+
}
|
|
306
|
+
function tokenizeGuardExpr(input) {
|
|
307
|
+
const tokens = [];
|
|
308
|
+
let i = 0;
|
|
309
|
+
while (i < input.length) {
|
|
310
|
+
if (/\s/.test(input[i])) {
|
|
311
|
+
i++;
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
if (input[i] === '"' || input[i] === "'") {
|
|
315
|
+
const quote = input[i];
|
|
316
|
+
let str = "";
|
|
317
|
+
i++;
|
|
318
|
+
while (i < input.length && input[i] !== quote) {
|
|
319
|
+
str += input[i];
|
|
320
|
+
i++;
|
|
321
|
+
}
|
|
322
|
+
i++;
|
|
323
|
+
tokens.push({ type: "string", value: str });
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
if (i + 1 < input.length) {
|
|
327
|
+
const two = input[i] + input[i + 1];
|
|
328
|
+
if (two === "==" || two === "!=" || two === "<=" || two === ">=") {
|
|
329
|
+
tokens.push({ type: "op", value: two });
|
|
330
|
+
i += 2;
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
if (input[i] === "<" || input[i] === ">") {
|
|
335
|
+
tokens.push({ type: "op", value: input[i] });
|
|
336
|
+
i++;
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
if (input[i] === "(") {
|
|
340
|
+
tokens.push({ type: "lparen", value: "(" });
|
|
341
|
+
i++;
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
if (input[i] === ")") {
|
|
345
|
+
tokens.push({ type: "rparen", value: ")" });
|
|
346
|
+
i++;
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
if (input[i] === ".") {
|
|
350
|
+
tokens.push({ type: "dot", value: "." });
|
|
351
|
+
i++;
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
if (/\d/.test(input[i]) || (input[i] === "-" && i + 1 < input.length && /\d/.test(input[i + 1]))) {
|
|
355
|
+
let num = input[i];
|
|
356
|
+
i++;
|
|
357
|
+
while (i < input.length && (/\d/.test(input[i]) || input[i] === ".")) {
|
|
358
|
+
num += input[i];
|
|
359
|
+
i++;
|
|
360
|
+
}
|
|
361
|
+
tokens.push({ type: "number", value: num });
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
if (/[a-zA-Z_]/.test(input[i])) {
|
|
365
|
+
let ident = "";
|
|
366
|
+
while (i < input.length && /[a-zA-Z0-9_]/.test(input[i])) {
|
|
367
|
+
ident += input[i];
|
|
368
|
+
i++;
|
|
369
|
+
}
|
|
370
|
+
tokens.push({ type: "ident", value: ident });
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
i++;
|
|
374
|
+
}
|
|
375
|
+
tokens.push({ type: "eof", value: "" });
|
|
376
|
+
return tokens;
|
|
377
|
+
}
|
|
378
|
+
function parseGuardExpression(input) {
|
|
379
|
+
const tokens = tokenizeGuardExpr(input);
|
|
380
|
+
let pos = 0;
|
|
381
|
+
function peek() { return tokens[pos]; }
|
|
382
|
+
function advance() { return tokens[pos++]; }
|
|
383
|
+
function parseOr() {
|
|
384
|
+
let left = parseAnd();
|
|
385
|
+
while (peek().type === "ident" && peek().value === "or") {
|
|
386
|
+
advance();
|
|
387
|
+
left = { kind: "or", left, right: parseAnd() };
|
|
388
|
+
}
|
|
389
|
+
return left;
|
|
390
|
+
}
|
|
391
|
+
function parseAnd() {
|
|
392
|
+
let left = parseNot();
|
|
393
|
+
while (peek().type === "ident" && peek().value === "and") {
|
|
394
|
+
advance();
|
|
395
|
+
left = { kind: "and", left, right: parseNot() };
|
|
396
|
+
}
|
|
397
|
+
return left;
|
|
398
|
+
}
|
|
399
|
+
function parseNot() {
|
|
400
|
+
if (peek().type === "ident" && peek().value === "not") {
|
|
401
|
+
advance();
|
|
402
|
+
return { kind: "not", expr: parsePrimary() };
|
|
403
|
+
}
|
|
404
|
+
return parsePrimary();
|
|
405
|
+
}
|
|
406
|
+
function parsePrimary() {
|
|
407
|
+
const tok = peek();
|
|
408
|
+
if (tok.type === "lparen") {
|
|
409
|
+
advance();
|
|
410
|
+
const expr = parseOr();
|
|
411
|
+
if (peek().type === "rparen")
|
|
412
|
+
advance();
|
|
413
|
+
return expr;
|
|
414
|
+
}
|
|
415
|
+
if (tok.type === "ident" && tok.value === "true") {
|
|
416
|
+
advance();
|
|
417
|
+
return { kind: "true" };
|
|
418
|
+
}
|
|
419
|
+
if (tok.type === "ident" && tok.value === "false") {
|
|
420
|
+
advance();
|
|
421
|
+
return { kind: "false" };
|
|
422
|
+
}
|
|
423
|
+
const varPath = parseVarPath();
|
|
424
|
+
if (peek().type === "ident" && peek().value === "is") {
|
|
425
|
+
advance();
|
|
426
|
+
if (peek().type === "ident" && peek().value === "not") {
|
|
427
|
+
advance();
|
|
428
|
+
if (peek().type === "ident" && peek().value === "null")
|
|
429
|
+
advance();
|
|
430
|
+
return { kind: "nullcheck", expr: varPath, isNull: false };
|
|
431
|
+
}
|
|
432
|
+
if (peek().type === "ident" && peek().value === "null") {
|
|
433
|
+
advance();
|
|
434
|
+
return { kind: "nullcheck", expr: varPath, isNull: true };
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
if (peek().type === "op") {
|
|
438
|
+
const op = advance().value;
|
|
439
|
+
const right = parseValue();
|
|
440
|
+
if (right.type === "null") {
|
|
441
|
+
return { kind: "nullcheck", expr: varPath, isNull: op === "==" };
|
|
442
|
+
}
|
|
443
|
+
return { kind: "compare", op: mapOp(op), left: varPath, right };
|
|
444
|
+
}
|
|
445
|
+
return { kind: "nullcheck", expr: varPath, isNull: false };
|
|
446
|
+
}
|
|
447
|
+
function parseVarPath() {
|
|
448
|
+
const parts = [];
|
|
449
|
+
if (peek().type === "ident") {
|
|
450
|
+
parts.push(advance().value);
|
|
451
|
+
while (peek().type === "dot") {
|
|
452
|
+
advance();
|
|
453
|
+
if (peek().type === "ident") {
|
|
454
|
+
parts.push(advance().value);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
return { kind: "variable", path: parts };
|
|
459
|
+
}
|
|
460
|
+
function parseValue() {
|
|
461
|
+
const tok = peek();
|
|
462
|
+
if (tok.type === "number") {
|
|
463
|
+
advance();
|
|
464
|
+
const num = parseFloat(tok.value);
|
|
465
|
+
return { kind: "value", type: "number", value: num };
|
|
466
|
+
}
|
|
467
|
+
if (tok.type === "string") {
|
|
468
|
+
advance();
|
|
469
|
+
return { kind: "value", type: "string", value: tok.value };
|
|
470
|
+
}
|
|
471
|
+
if (tok.type === "ident") {
|
|
472
|
+
advance();
|
|
473
|
+
if (tok.value === "null")
|
|
474
|
+
return { kind: "value", type: "null", value: null };
|
|
475
|
+
if (tok.value === "true")
|
|
476
|
+
return { kind: "value", type: "boolean", value: true };
|
|
477
|
+
if (tok.value === "false")
|
|
478
|
+
return { kind: "value", type: "boolean", value: false };
|
|
479
|
+
return { kind: "value", type: "string", value: tok.value };
|
|
480
|
+
}
|
|
481
|
+
advance();
|
|
482
|
+
return { kind: "value", type: "null", value: null };
|
|
483
|
+
}
|
|
484
|
+
function mapOp(op) {
|
|
485
|
+
switch (op) {
|
|
486
|
+
case "==": return "eq";
|
|
487
|
+
case "!=": return "ne";
|
|
488
|
+
case "<": return "lt";
|
|
489
|
+
case ">": return "gt";
|
|
490
|
+
case "<=": return "le";
|
|
491
|
+
case ">=": return "ge";
|
|
492
|
+
default: return "eq";
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
return parseOr();
|
|
496
|
+
}
|
|
497
|
+
function parseMdActionSignature(name, text) {
|
|
498
|
+
text = text.trim();
|
|
499
|
+
const parenStart = text.indexOf('(');
|
|
500
|
+
const parenEnd = text.indexOf(')');
|
|
501
|
+
const paramsStr = text.slice(parenStart + 1, parenEnd).trim();
|
|
502
|
+
const parameters = [];
|
|
503
|
+
if (paramsStr) {
|
|
504
|
+
for (const param of paramsStr.split(',')) {
|
|
505
|
+
const paramName = param.trim().split(':')[0].trim();
|
|
506
|
+
if (paramName)
|
|
507
|
+
parameters.push(paramName);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
const afterParen = text.slice(parenEnd + 1).trim();
|
|
511
|
+
const arrowIdx = afterParen.indexOf('->');
|
|
512
|
+
const returnPart = afterParen.slice(arrowIdx + 2).trim();
|
|
513
|
+
let returnType = 'Context';
|
|
514
|
+
let hasEffect = false;
|
|
515
|
+
let effectType;
|
|
516
|
+
const plusIdx = returnPart.indexOf('+');
|
|
517
|
+
if (plusIdx !== -1) {
|
|
518
|
+
returnType = returnPart.slice(0, plusIdx).trim();
|
|
519
|
+
const effectMatch = returnPart.slice(plusIdx + 1).trim().match(/Effect<(\w+)>/);
|
|
520
|
+
if (effectMatch) {
|
|
521
|
+
hasEffect = true;
|
|
522
|
+
effectType = effectMatch[1];
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
else {
|
|
526
|
+
returnType = returnPart;
|
|
527
|
+
}
|
|
528
|
+
return { name, parameters, returnType, hasEffect, effectType };
|
|
529
|
+
}
|
|
530
|
+
function parseMachineFromElements(elements) {
|
|
531
|
+
let machineName = 'unknown';
|
|
532
|
+
const context = {};
|
|
533
|
+
const events = [];
|
|
534
|
+
const transitions = [];
|
|
535
|
+
const guards = {};
|
|
536
|
+
const actions = [];
|
|
537
|
+
const effects = [];
|
|
538
|
+
const stateEntries = [];
|
|
539
|
+
let currentStateEntry = null;
|
|
540
|
+
for (let i = 0; i < elements.length; i++) {
|
|
541
|
+
const el = elements[i];
|
|
542
|
+
if (el.kind === 'heading') {
|
|
543
|
+
// Machine heading
|
|
544
|
+
if (el.level === 1 && el.text.startsWith('machine ')) {
|
|
545
|
+
machineName = el.text.slice(8).trim();
|
|
546
|
+
currentStateEntry = null;
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
// Section headings
|
|
550
|
+
const sectionName = el.text.toLowerCase();
|
|
551
|
+
if (['context', 'events', 'transitions', 'guards', 'actions', 'effects'].includes(sectionName)) {
|
|
552
|
+
currentStateEntry = null;
|
|
553
|
+
const nextEl = elements[i + 1];
|
|
554
|
+
if (sectionName === 'context' && nextEl?.kind === 'table') {
|
|
555
|
+
const fi = findColumnIndex(nextEl.headers, 'field');
|
|
556
|
+
const ti = findColumnIndex(nextEl.headers, 'type');
|
|
557
|
+
const di = findColumnIndex(nextEl.headers, 'default');
|
|
558
|
+
for (const row of nextEl.rows) {
|
|
559
|
+
const name = row[fi]?.trim() || '';
|
|
560
|
+
const defaultStr = di >= 0 ? row[di]?.trim() : '';
|
|
561
|
+
let defaultValue = null;
|
|
562
|
+
if (defaultStr) {
|
|
563
|
+
if (/^\d+$/.test(defaultStr))
|
|
564
|
+
defaultValue = parseInt(defaultStr, 10);
|
|
565
|
+
else if (/^\d+\.\d+$/.test(defaultStr))
|
|
566
|
+
defaultValue = parseFloat(defaultStr);
|
|
567
|
+
else if (defaultStr === 'true' || defaultStr === 'false')
|
|
568
|
+
defaultValue = defaultStr === 'true';
|
|
569
|
+
else if (defaultStr.startsWith('"') || defaultStr.startsWith("'"))
|
|
570
|
+
defaultValue = defaultStr.slice(1, -1);
|
|
571
|
+
else
|
|
572
|
+
defaultValue = defaultStr;
|
|
573
|
+
}
|
|
574
|
+
context[name] = defaultValue;
|
|
575
|
+
}
|
|
576
|
+
i++;
|
|
577
|
+
}
|
|
578
|
+
else if (sectionName === 'events' && nextEl?.kind === 'bullets') {
|
|
579
|
+
for (const item of nextEl.items) {
|
|
580
|
+
for (const name of item.split(',').map(n => n.trim()).filter(Boolean)) {
|
|
581
|
+
events.push(name);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
i++;
|
|
585
|
+
}
|
|
586
|
+
else if (sectionName === 'transitions' && nextEl?.kind === 'table') {
|
|
587
|
+
const si = findColumnIndex(nextEl.headers, 'source');
|
|
588
|
+
const ei = findColumnIndex(nextEl.headers, 'event');
|
|
589
|
+
const gi = findColumnIndex(nextEl.headers, 'guard');
|
|
590
|
+
const ti = findColumnIndex(nextEl.headers, 'target');
|
|
591
|
+
const ai = findColumnIndex(nextEl.headers, 'action');
|
|
592
|
+
for (const row of nextEl.rows) {
|
|
593
|
+
const t = {
|
|
594
|
+
source: row[si]?.trim() || '',
|
|
595
|
+
event: row[ei]?.trim() || '',
|
|
596
|
+
guard: row[gi]?.trim() || undefined,
|
|
597
|
+
target: row[ti]?.trim() || '',
|
|
598
|
+
action: row[ai]?.trim() || undefined,
|
|
599
|
+
};
|
|
600
|
+
if (!t.guard)
|
|
601
|
+
delete t.guard;
|
|
602
|
+
if (!t.action || t.action === '_')
|
|
603
|
+
delete t.action;
|
|
604
|
+
transitions.push(t);
|
|
605
|
+
}
|
|
606
|
+
i++;
|
|
607
|
+
}
|
|
608
|
+
else if (sectionName === 'guards' && nextEl?.kind === 'table') {
|
|
609
|
+
const ni = findColumnIndex(nextEl.headers, 'name');
|
|
610
|
+
const ei = findColumnIndex(nextEl.headers, 'expression');
|
|
611
|
+
for (const row of nextEl.rows) {
|
|
612
|
+
const name = row[ni]?.trim() || '';
|
|
613
|
+
const exprStr = stripBackticks(row[ei]?.trim() || '');
|
|
614
|
+
guards[name] = parseGuardExpression(exprStr);
|
|
615
|
+
}
|
|
616
|
+
i++;
|
|
617
|
+
}
|
|
618
|
+
else if (sectionName === 'actions' && nextEl?.kind === 'table') {
|
|
619
|
+
const ni = findColumnIndex(nextEl.headers, 'name');
|
|
620
|
+
const si = findColumnIndex(nextEl.headers, 'signature');
|
|
621
|
+
for (const row of nextEl.rows) {
|
|
622
|
+
actions.push(parseMdActionSignature(row[ni]?.trim() || '', stripBackticks(row[si]?.trim() || '')));
|
|
623
|
+
}
|
|
624
|
+
i++;
|
|
625
|
+
}
|
|
626
|
+
else if (sectionName === 'effects' && nextEl?.kind === 'table') {
|
|
627
|
+
const ni = findColumnIndex(nextEl.headers, 'name');
|
|
628
|
+
const ii = findColumnIndex(nextEl.headers, 'input');
|
|
629
|
+
const oi = findColumnIndex(nextEl.headers, 'output');
|
|
630
|
+
for (const row of nextEl.rows) {
|
|
631
|
+
effects.push({
|
|
632
|
+
name: stripBackticks(row[ni]?.trim() || ''),
|
|
633
|
+
input: stripBackticks(row[ii]?.trim() || ''),
|
|
634
|
+
output: stripBackticks(row[oi]?.trim() || ''),
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
i++;
|
|
638
|
+
}
|
|
639
|
+
continue;
|
|
640
|
+
}
|
|
641
|
+
// State heading
|
|
642
|
+
const stateMatch = el.text.match(/^state\s+(\w+)(.*)$/);
|
|
643
|
+
if (stateMatch) {
|
|
644
|
+
currentStateEntry = {
|
|
645
|
+
type: 'state', level: el.level, name: stateMatch[1],
|
|
646
|
+
...parseMdAnnotations(stateMatch[2]?.trim() || ''),
|
|
647
|
+
};
|
|
648
|
+
stateEntries.push(currentStateEntry);
|
|
649
|
+
continue;
|
|
650
|
+
}
|
|
651
|
+
// Region heading
|
|
652
|
+
const regionMatch = el.text.match(/^region\s+(\w+)$/);
|
|
653
|
+
if (regionMatch) {
|
|
654
|
+
currentStateEntry = null;
|
|
655
|
+
stateEntries.push({
|
|
656
|
+
type: 'region', level: el.level, name: regionMatch[1],
|
|
657
|
+
isInitial: false, isFinal: false, isParallel: false,
|
|
658
|
+
});
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
661
|
+
currentStateEntry = null;
|
|
662
|
+
continue;
|
|
663
|
+
}
|
|
664
|
+
// Content belonging to current state
|
|
665
|
+
if (currentStateEntry) {
|
|
666
|
+
if (el.kind === 'blockquote') {
|
|
667
|
+
currentStateEntry.description = el.text;
|
|
668
|
+
}
|
|
669
|
+
else if (el.kind === 'bullets') {
|
|
670
|
+
for (const item of el.items)
|
|
671
|
+
parseMdStateBullet(currentStateEntry, item);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
// Build state hierarchy
|
|
676
|
+
const baseLevel = stateEntries.length > 0 ? stateEntries[0].level : 2;
|
|
677
|
+
const states = buildMdStatesAtLevel(stateEntries, 0, baseLevel).states;
|
|
678
|
+
return { name: machineName, context, events, states, transitions, guards, actions, effects };
|
|
679
|
+
}
|
|
680
|
+
/**
|
|
681
|
+
* Parse Orca markdown (.orca.md) format into a MachineDef.
|
|
682
|
+
* For multi-machine files, returns the first machine.
|
|
683
|
+
*/
|
|
684
|
+
export function parseOrcaMd(source) {
|
|
685
|
+
const elements = parseMarkdownStructure(source);
|
|
686
|
+
// Split by separators for multi-machine files
|
|
687
|
+
const chunks = [];
|
|
688
|
+
let currentChunk = [];
|
|
689
|
+
for (const el of elements) {
|
|
690
|
+
if (el.kind === 'separator') {
|
|
691
|
+
if (currentChunk.length > 0) {
|
|
692
|
+
chunks.push(currentChunk);
|
|
693
|
+
currentChunk = [];
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
else {
|
|
697
|
+
currentChunk.push(el);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
if (currentChunk.length > 0) {
|
|
701
|
+
chunks.push(currentChunk);
|
|
702
|
+
}
|
|
703
|
+
// Parse first chunk as machine
|
|
704
|
+
return parseMachineFromElements(chunks[0] || []);
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* Parse Orca markdown (.orca.md) format into multiple MachineDefs.
|
|
708
|
+
* For single-machine files, returns an array with one machine.
|
|
709
|
+
*/
|
|
710
|
+
export function parseOrcaMdMulti(source) {
|
|
711
|
+
const elements = parseMarkdownStructure(source);
|
|
712
|
+
// Split by separators for multi-machine files
|
|
713
|
+
const chunks = [];
|
|
714
|
+
let currentChunk = [];
|
|
715
|
+
for (const el of elements) {
|
|
716
|
+
if (el.kind === 'separator') {
|
|
717
|
+
if (currentChunk.length > 0) {
|
|
718
|
+
chunks.push(currentChunk);
|
|
719
|
+
currentChunk = [];
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
else {
|
|
723
|
+
currentChunk.push(el);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
if (currentChunk.length > 0) {
|
|
727
|
+
chunks.push(currentChunk);
|
|
728
|
+
}
|
|
729
|
+
return chunks.map(chunk => parseMachineFromElements(chunk));
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* Parse Orca machine definition (markdown format).
|
|
733
|
+
*/
|
|
734
|
+
export function parseOrcaAuto(source) {
|
|
735
|
+
return parseOrcaMd(source);
|
|
736
|
+
}
|
|
737
|
+
//# sourceMappingURL=parser.js.map
|