dal-ast-js 0.0.1-dev

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/index.cjs ADDED
@@ -0,0 +1,696 @@
1
+ 'use strict';
2
+
3
+ let TOKENS = {
4
+ "LPAREN": "(",
5
+ "RPAREN": ")",
6
+ "QUOTE": "\"",
7
+ "COMMA": ",",
8
+ "LBRACE": "{",
9
+ "RBRACE": "}",
10
+ "LBRACKET": "[",
11
+ "RBRACKET": "]"
12
+ };
13
+ TOKENS = Object.freeze(TOKENS);
14
+
15
+ var TOKENS$1 = TOKENS;
16
+
17
+ /**
18
+ * This lexer works as follows:
19
+ * - Reads character at current position
20
+ * - If it is not a token:
21
+ * - adds to identifier accumulator
22
+ * - If it is a token:
23
+ * - saves what is in the accumulator to scannedTokens
24
+ * - saves the token to scannedTokens
25
+ * - If a token is a quote:
26
+ * - saves quote to scanned token
27
+ * - scans forward to end of quote while accumulating identifier
28
+ * - saves accumulator to scanned tokens
29
+ * - saves quote to scanned tokens
30
+ *
31
+ * I am not scanning for keywords here, I just save them as
32
+ * identifiers and then in the parse stage I will classify them.
33
+ * This makes the algorithm very simple.
34
+ *
35
+ * Example:
36
+ * design ("name")
37
+ * IDENTIFIER LPAREN QUOTE IDENTIFIER QUOTE RPAREN
38
+ *
39
+ * I also save the line/col number (starting and ending). This will be
40
+ * useful for visualization in the workbench.
41
+ */
42
+ class DalLexer {
43
+ constructor (source) {
44
+ this.currPos = 0;
45
+ this.source = [...source];
46
+ this.scannedTokens = [];
47
+
48
+ // Current line and colno
49
+ this.lineno = 1;
50
+ this.colno = 0;
51
+
52
+ // Accumulates identifiers until tokens are visited.
53
+ this.accumulatedIdentifier = [];
54
+ this.startColIdentifier;
55
+ this.startLineIdentifier;
56
+
57
+ this.run();
58
+ }
59
+
60
+ /**
61
+ * Runs the lexer from the current position.
62
+ */
63
+ run() {
64
+ do {
65
+ const character = this.source[this.currPos];
66
+ this.processCurrentPosition(character);
67
+ } while (++this.currPos < this.source.length);
68
+ }
69
+
70
+ /**
71
+ * Processes the character at the current position.
72
+ */
73
+ processCurrentPosition(character) {
74
+ this.colno++;
75
+
76
+ if (character == " ") {
77
+ this.addAccumulatedIdentifierToken();
78
+ return;
79
+ } else if (character == "\n") {
80
+ this.lineno++;
81
+ this.colno = 0;
82
+ return;
83
+ }
84
+
85
+ const token = this.getToken(character);
86
+ if (token) {
87
+ this.addAccumulatedIdentifierToken();
88
+ this.addToken(token);
89
+ if (token === "QUOTE") {
90
+ this.extractStringFromQuotes();
91
+ }
92
+ return;
93
+ }
94
+ this.addToAccumulator(character);
95
+ }
96
+
97
+ /**
98
+ * Finds the identifier given the character.
99
+ * @param {String} character Character from scan.
100
+ * @returns {String|null} Identifier if valid token, else null.
101
+ */
102
+ getToken (character) {
103
+ for (const [identifier, value] of Object.entries(TOKENS$1)) {
104
+ if (value[0] !== character) {
105
+ continue;
106
+ }
107
+ return identifier;
108
+ }
109
+ }
110
+
111
+ /**
112
+ * If a quote is encountered, scan forward until end of quote is found.
113
+ * Everything inside the quote is part of the string, this ignores any
114
+ * of the tokens like comma.
115
+ *
116
+ * TODO: Add support for escaped quotes inside quotes.
117
+ */
118
+ extractStringFromQuotes () {
119
+ this.colno++;
120
+ this.currPos++;
121
+ do {
122
+ const character = this.source[this.currPos];
123
+ const identifier = this.getToken(character);
124
+ if (character === "\n") {
125
+ this.lineno++;
126
+ this.colno = 0;
127
+ continue;
128
+ }
129
+ if (identifier !== "QUOTE") {
130
+ this.addToAccumulator(character);
131
+ this.colno++;
132
+ continue;
133
+ }
134
+ this.addAccumulatedIdentifierToken();
135
+ this.addToken(identifier);
136
+ break;
137
+ } while (this.currPos++ < this.source.length)
138
+ }
139
+
140
+ /**
141
+ * Adds to accumulator. Saves starting position of
142
+ * identifier being accumulated.
143
+ *
144
+ * @param {String} character Character to add to accumulate.
145
+ */
146
+ addToAccumulator (character) {
147
+ if (this.accumulatedIdentifier.length === 0) {
148
+ this.startColIdentifier = this.colno;
149
+ this.startLineIdentifier = this.lineno;
150
+ }
151
+ this.accumulatedIdentifier.push(character);
152
+ }
153
+
154
+ /**
155
+ * Add the accumulated identifier to the scannedTokens list.
156
+ */
157
+ addAccumulatedIdentifierToken () {
158
+ // Subtract 1 from endColno beause we have to reach token
159
+ // to identify that the accumulator is done.
160
+ if (this.accumulatedIdentifier.length > 0) {
161
+ this.scannedTokens.push({
162
+ type: "IDENTIFIER",
163
+ value: this.accumulatedIdentifier.join(""),
164
+ startLineno: this.startLineIdentifier,
165
+ startColno: this.startColIdentifier,
166
+ endLineno: this.lineno,
167
+ endColno: this.colno - 1
168
+ });
169
+ this.accumulatedIdentifier = [];
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Adds the token to the scannedTokens list.
175
+ * @param {String} type Type for the token.
176
+ * @param {Number} value Value of token (example identifier)
177
+ */
178
+ addToken (type, value) {
179
+ this.scannedTokens.push({
180
+ type: type,
181
+ value: value,
182
+ startLineno: this.lineno,
183
+ startColno: this.colno,
184
+ endLineno: this.lineno,
185
+ endColno: this.colno
186
+ });
187
+ }
188
+ }
189
+
190
+ let KEYWORDS = [
191
+ "design",
192
+ "behavior",
193
+ "if",
194
+ "else",
195
+ "for",
196
+ "while"
197
+ ];
198
+ KEYWORDS = Object.freeze(KEYWORDS);
199
+
200
+ /**
201
+ * Builds string back from the tokens.
202
+ *
203
+ * Ex:
204
+ * LBRACKET QUOTE IDENTIFIER("test") QUOTE RBRACKET
205
+ * ("test")
206
+ */
207
+ class Untokenizer {
208
+
209
+ constructor (tokens) {
210
+ this.tokens = tokens;
211
+ }
212
+
213
+ /**
214
+ * Builds string from tokens.
215
+ * @param {Array} tokens
216
+ * @returns
217
+ */
218
+ buildString(tokens) {
219
+ let str = "";
220
+
221
+ for (const token of this.tokens) {
222
+ if (token.type === "IDENTIFIER") {
223
+ str = str + token.value;
224
+ } else {
225
+ str = str + TOKENS$1[token.type];
226
+ }
227
+ }
228
+
229
+ return str;
230
+ }
231
+ }
232
+
233
+ /**
234
+ * Structure
235
+ * command(args...)
236
+ *
237
+ * Example:
238
+ * set(book, ["name"], "Harry Potter")
239
+ *
240
+ * Returns:
241
+ * [
242
+ * {
243
+ * "type": "name",
244
+ * "value": "book"
245
+ * },
246
+ * {
247
+ * "type": "list",
248
+ * "value": ["name"]
249
+ * },
250
+ * {
251
+ * "type": "string",
252
+ * "value": "Harry Potter"
253
+ * }
254
+ * ]
255
+ *
256
+ * This class parses the arguments from parser
257
+ * for a keyword or command instruction.
258
+ */
259
+ class ArgsParser {
260
+ constructor (tokens) {
261
+ this.tokens = tokens;
262
+ this.groupedTokens = [];
263
+ this.parsedArgs = [];
264
+
265
+ this.pairs = [
266
+ {
267
+ start:"QUOTE",
268
+ end:"QUOTE",
269
+ type:"string"
270
+ },
271
+ {
272
+ start:"LBRACE",
273
+ end:"RBRACE",
274
+ type:"object"
275
+ },
276
+ {
277
+ start:"LBRACKET",
278
+ end:"RBRACKET",
279
+ type: "list"
280
+ }
281
+ ];
282
+ }
283
+
284
+ run () {
285
+ if (this.tokens.length === 0) {
286
+ return this.parsedArgs;
287
+ }
288
+ this.splitByComma(this.tokens);
289
+ for (const group of this.groupedTokens) {
290
+ this.parsedArgs.push(this.processGroup(group));
291
+ }
292
+ return this.parsedArgs;
293
+ }
294
+
295
+ /**
296
+ * Splits the tokens by commas so they can be processed independently.
297
+ * For objects, arrays and strings, it flags that pair is being tracked
298
+ * so that any commmas inside the object or array is ignored.
299
+ * @param {Array} tokens
300
+ */
301
+ splitByComma (tokens) {
302
+ let pos = 0;
303
+ let group = [];
304
+ this.trackingPair = null;
305
+ do {
306
+ const token = this.tokens[pos];
307
+ if (token.type === "COMMA" && !this.trackingPair) {
308
+ this.groupedTokens.push(group);
309
+ group = [];
310
+ continue;
311
+ }
312
+
313
+ if (this.trackingPair && token.type === this.trackingPair) {
314
+ this.trackingPair = null;
315
+ } else if (!this.trackingPair) {
316
+ this.trackingPair = this.isPair(token);
317
+ }
318
+
319
+ group.push(token);
320
+
321
+ } while (++pos < this.tokens.length);
322
+
323
+ this.groupedTokens.push(group);
324
+ }
325
+
326
+ /**
327
+ * Checks if the token is a pair.
328
+ * @param {Object} token Token being checked.
329
+ * @returns {String|null}
330
+ */
331
+ isPair (token) {
332
+ for(const pair of this.pairs) {
333
+ if (token.type === pair.start) {
334
+ return pair.end;
335
+ }
336
+ }
337
+ }
338
+
339
+ /**
340
+ * Process the grouped tokens and returns
341
+ * the type and value of the arguments.
342
+ *
343
+ * Types:
344
+ * ------
345
+ * Name (this is a participant name)
346
+ * String
347
+ * List or Array
348
+ * Object
349
+ * Number
350
+ *
351
+ * @param {Array} tokens
352
+ */
353
+ processGroup (tokens) {
354
+ let type;
355
+ let value = new Untokenizer(tokens).buildString();
356
+ if (tokens[0].type === "QUOTE") {
357
+ type = "string";
358
+ value = JSON.parse(value);
359
+ } else if (tokens[0].type === "LBRACKET") {
360
+ type = "list";
361
+ value = JSON.parse(value);
362
+ } else if (tokens[0].type === "LBRACE") {
363
+ type = "object";
364
+ value = JSON.parse(value);
365
+ } else if (!Number.isNaN(Number(value))) {
366
+ type = "number";
367
+ value = parseFloat(value);
368
+ } else {
369
+ type = "name";
370
+ if (value === "true") {
371
+ value = true;
372
+ } else if (value === "false") {
373
+ value = false;
374
+ }
375
+ }
376
+
377
+ return {
378
+ type: type,
379
+ value: value
380
+ }
381
+ }
382
+ }
383
+
384
+ /**
385
+ * This class produces an AST given the scanned tokens.
386
+ *
387
+ * Steps:
388
+ * - Process current token
389
+ * - Visit keyword and create node
390
+ * - Parse the args and save node metadata
391
+ * - If node has a body (if, behavior), add to stack
392
+ * - If node doesn't have body, add to body of stack top
393
+ * - When rbrace is encountered, pop stack and add
394
+ * to body of top of stack.
395
+ *
396
+ * It is a very simple algorithm because my language is basic.
397
+ * I create stack to track the nested body being processed.
398
+ * I use rbrace as marker to finish a nested block and build the tree.
399
+ *
400
+ * Note: This was the first way I thought to implement this
401
+ * but I think there is a better way, I am going to iterate
402
+ * on this. This approach does not identify syntax errors
403
+ * properly.
404
+ *
405
+ * The actual commands are very simple in this language
406
+ * because I am always following the format shown below:
407
+ *
408
+ * command(args)
409
+ *
410
+ * So every identifier that isn't a keyword while scanning
411
+ * forward is a command.
412
+ */
413
+ class DalParser {
414
+
415
+ constructor (tokens) {
416
+ this.tokens = tokens;
417
+ this.currPos = 0;
418
+ this.ast = {
419
+ type: "root",
420
+ body: []
421
+ };
422
+ this.stack = [this.ast];
423
+ this.run();
424
+ }
425
+
426
+ run () {
427
+ do {
428
+ const token = this.tokens[this.currPos];
429
+ this.processToken(token);
430
+ } while (++this.currPos < this.tokens.length);
431
+ }
432
+
433
+ /**
434
+ * Process the current token.
435
+ *
436
+ * RBRACE indicates current block is over.
437
+ * behavior and if are blocks and have a body
438
+ * design is a statement.
439
+ *
440
+ * @param {String} token
441
+ */
442
+ processToken(token) {
443
+ if (token.type === "RBRACE") {
444
+ this.closeBlock();
445
+ } else if (token.value === "design") {
446
+ this.processDesignKeyword();
447
+ } else if (token.value === "behavior") {
448
+ this.processBehavior();
449
+ } else if (token.value === "if") {
450
+ this.processIf();
451
+ } else if (token.value === "else") {
452
+ this.processElse();
453
+ } else if (token.value === "for") {
454
+ this.processFor();
455
+ } else if (token.value === "while") {
456
+ this.processWhile();
457
+ }else if (token.type === "IDENTIFIER") {
458
+ this.processCmd(token.value);
459
+ }
460
+ }
461
+
462
+
463
+ /**
464
+ * Closes the nested block by removing it from
465
+ * the top of the stack and adding it to the new
466
+ * node at the top of the stack.
467
+ *
468
+ * It uses the RBRACE identifier to determine if
469
+ * the top block is closed.
470
+ *
471
+ * For if statements, it appends the elif and else
472
+ * blocks to the if block that was already processed
473
+ * so that they can be synthesized in a single AST node.
474
+ */
475
+ closeBlock () {
476
+ const node = this.stack.pop();
477
+
478
+ if (node.type === "else") {
479
+ const body = this.stack[this.stack.length - 1].body;
480
+ const ifNode = body[body.length - 1];
481
+ ifNode["else"] = node;
482
+ } else {
483
+ this.stack[this.stack.length - 1].body.push(node);
484
+ }
485
+ }
486
+
487
+ /**
488
+ * Processes the behavior block.
489
+ *
490
+ * behavior <behavior_name>(args1, arg2...argN) {
491
+ *
492
+ * }
493
+ */
494
+ processBehavior () {
495
+ const behaviorName = this.tokens[++this.currPos].value;
496
+ const node = {
497
+ type: "behavior",
498
+ behaviorName: behaviorName,
499
+ body: []
500
+ };
501
+ this.stack.push(node);
502
+ }
503
+
504
+ /**
505
+ * Processes if block.
506
+ *
507
+ * if (condition) {
508
+ *
509
+ * }
510
+ */
511
+ processIf () {
512
+ const parsedArgs = new ArgsParser(this.getArgs()).run();
513
+ const node = {
514
+ type: "if",
515
+ args: parsedArgs,
516
+ body: []
517
+ };
518
+ this.stack.push(node);
519
+ }
520
+
521
+ /**
522
+ * Processes while block.
523
+ *
524
+ * while (condition) {
525
+ *
526
+ * }
527
+ */
528
+ processWhile () {
529
+ const parsedArgs = new ArgsParser(this.getArgs()).run();
530
+ const node = {
531
+ type: "while",
532
+ args: parsedArgs,
533
+ body: []
534
+ };
535
+ this.stack.push(node);
536
+ }
537
+
538
+ /**
539
+ * Processes else block.
540
+ *
541
+ * else (condition) {
542
+ *
543
+ * }
544
+ */
545
+ processElse () {
546
+ const node = {
547
+ type: "else",
548
+ body: []
549
+ };
550
+ this.stack.push(node);
551
+ }
552
+
553
+ /**
554
+ * Processes for block.
555
+ *
556
+ * for (<participan>, <start>,<end>) {
557
+ *
558
+ * }
559
+ */
560
+ processFor () {
561
+ const parsedArgs = new ArgsParser(this.getArgs()).run();
562
+ const node = {
563
+ type: "for",
564
+ participant: parsedArgs[0],
565
+ start: parsedArgs[1],
566
+ end: parsedArgs[2],
567
+ body: []
568
+ };
569
+ this.stack.push(node);
570
+ }
571
+
572
+ /**
573
+ * Process design keyword.
574
+ *
575
+ * design(<design_name>)
576
+ */
577
+ processDesignKeyword () {
578
+ const parsedArgs = new ArgsParser(this.getArgs()).run();
579
+ const node = {
580
+ "type": "design",
581
+ "design_name": parsedArgs
582
+ };
583
+ this.stack[this.stack.length - 1].body.push(node);
584
+ }
585
+
586
+ /**
587
+ * Process design keyword.
588
+ *
589
+ * cmd(args1, arg2...argN)
590
+ */
591
+ processCmd (cmd) {
592
+ const parsedArgs = new ArgsParser(this.getArgs()).run();
593
+ const node = {
594
+ "type": "cmd",
595
+ "command": cmd,
596
+ "args": parsedArgs
597
+ };
598
+ this.stack[this.stack.length - 1].body.push(node);
599
+ }
600
+
601
+ /**
602
+ * Parse the args.
603
+ *
604
+ * command("test",varName, 123,["count"])
605
+ *
606
+ * "args": [
607
+ * {
608
+ * "type": "string",
609
+ * "value": "test"
610
+ * },
611
+ * {
612
+ * "type": "name",
613
+ * "value": "varName"
614
+ * },
615
+ * {
616
+ * "type": "number",
617
+ * "value": 123
618
+ * },
619
+ * {
620
+ * "type": "list",
621
+ * "value": [
622
+ * "count"
623
+ * ]
624
+ * }
625
+ * ]
626
+ *
627
+ * @returns {Object} Returns the processed args.
628
+ */
629
+ getArgs () {
630
+ const token = this.tokens[++this.currPos];
631
+ if (token.type !== "LPAREN") {
632
+ throw new Error("Expected LPAREN, Syntax error")
633
+ }
634
+
635
+ const args = [];
636
+ let foundRParen = false;
637
+ do {
638
+ const token = this.tokens[++this.currPos];
639
+ if (token.type === "RPAREN") {
640
+ foundRParen = true;
641
+ break;
642
+ } else {
643
+ args.push(token);
644
+ }
645
+ } while (this.currPos < this.tokens.length)
646
+
647
+ // This is a crude attempt to see if brackets are closed
648
+ // It has obvious flaws, if the parenthesis isn't closed and
649
+ // then another identifier opens and closes parenthesis, it
650
+ // will keep moving forward until it reaches it.
651
+
652
+ // There is a better way to do this, so this will get replaced.
653
+ if (!foundRParen) {
654
+ throw new Error("Expected RPAREN, Syntax error")
655
+ }
656
+
657
+ return args;
658
+ }
659
+
660
+ }
661
+
662
+ /**
663
+ * This class produces an AST output given a DAL source file.
664
+ *
665
+ * Internally it invokes:
666
+ * - Lexer
667
+ * - Parser
668
+ * - AST output
669
+ *
670
+ * The AST output is sent to the synthesizer and synthesized
671
+ * into a python program.
672
+ *
673
+ * In the future, this will also:
674
+ * - Identify syntax errors
675
+ * - Lint
676
+ * - Validate the internal consistency of the design
677
+ */
678
+ class DalAstGenerator {
679
+
680
+ constructor() {
681
+ this.ast = null;
682
+ }
683
+
684
+ run (source) {
685
+ try {
686
+ const lexer = new DalLexer(source);
687
+ const parser = new DalParser(lexer.scannedTokens);
688
+ this.ast = parser.ast;
689
+ } catch(e) {
690
+ console.error("Error generating the AST:", e);
691
+ }
692
+ return this.ast;
693
+ }
694
+ }
695
+
696
+ exports.DalAstGenerator = DalAstGenerator;