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