├── .editorconfig ├── .eslintrc ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── src ├── ast │ ├── Assign.js │ ├── BinaryOperator.js │ ├── Block.js │ ├── Compound.js │ ├── NoOperation.js │ ├── Node.js │ ├── Number.js │ ├── Param.js │ ├── ProcedureDecl.js │ ├── Program.js │ ├── Type.js │ ├── UnaryOperator.js │ ├── VarDecl.js │ ├── Variable.js │ └── index.js ├── index.js ├── interpreter │ ├── Interpreter.js │ └── index.js ├── lexer │ ├── Lexer.js │ ├── Token.js │ └── index.js ├── parser │ ├── Parser.js │ └── index.js ├── semantic │ └── SemanticAnalyzer.js └── symbols │ ├── ProcedureSymbol.js │ ├── Symbol.js │ ├── SymbolTable.js │ ├── SymbolTableBuilder.js │ ├── TypeSymbol.js │ └── VariableSymbol.js └── test ├── .eslintrc ├── Entry.test.js ├── ast ├── Assign.test.js ├── BinaryOperator.test.js ├── Block.test.js ├── Compound.test.js ├── NoOperation.test.js ├── Node.test.js ├── Number.test.js ├── Param.test.js ├── ProcedureDecl.test.js ├── Program.test.js ├── Type.test.js ├── UnaryOperator.test.js ├── VarDecl.test.js ├── Variable.test.js └── index.test.js ├── interpreter └── Interpreter.test.js ├── lexer ├── Lexer.test.js └── Token.test.js ├── mocha.opts ├── parser └── Parser.test.js ├── semantic └── SemanticAnalyzer.test.js └── symbols ├── ProcedureSymbol.test.js ├── Symbol.test.js ├── SymbolTable.test.js ├── SymbolTableBuilder.test.js ├── TypeSymbol.test.js └── VariableSymbol.test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "node", 3 | "rules": { 4 | "import/no-commonjs": [ 5 | "off" 6 | ], 7 | "class-methods-use-this": [ 8 | "off" 9 | ] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # Misc 61 | .idea 62 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | cache: 4 | directories: 5 | - node_modules 6 | notifications: 7 | email: true 8 | node_js: 9 | - stable 10 | before_script: 11 | - npm prune 12 | after_success: 13 | - npm run coveralls 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017-present Eugene Obrezkov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pascal Interpreter 2 | 3 | [![Build Status](https://travis-ci.org/ghaiklor/pascal-interpreter.svg?branch=master)](https://travis-ci.org/ghaiklor/pascal-interpreter) 4 | [![Coverage Status](https://coveralls.io/repos/github/ghaiklor/pascal-interpreter/badge.svg?branch=master)](https://coveralls.io/github/ghaiklor/pascal-interpreter?branch=master) 5 | [![Greenkeeper badge](https://badges.greenkeeper.io/ghaiklor/pascal-interpreter.svg)](https://greenkeeper.io/) 6 | 7 | _A simple interpreter for a large subset of Pascal language written for educational purposes_ 8 | 9 | _This repository contains the code I wrote during these articles - [https://ruslanspivak.com/lsbasi-part1/](https://ruslanspivak.com/lsbasi-part1/)_ 10 | 11 | ## Demo 12 | 13 | For demonstration purposes I wrote the following code in Pascal: 14 | 15 | ```pascal 16 | PROGRAM myProgram; 17 | VAR 18 | number : INTEGER; 19 | a, b : INTEGER; 20 | y : REAL; 21 | 22 | BEGIN {myProgram} 23 | number := 2; 24 | a := number; 25 | b := 10 * a + 10 * a DIV 4; 26 | y := 20 / 7 + 3.14; 27 | END. {myProgram} 28 | ``` 29 | 30 | Since, there are no I\O implementation in Pascal, I'll show you a GIF where you can see scope of variables before and after interpretation of this program. 31 | 32 | Here is the copied output from my demo script (gif below): 33 | 34 | ``` 35 | ---------------------------------- 36 | Program scope before interpreting: 37 | undefined 38 | ---------------------------------- 39 | Program scope after interpreting: 40 | { number: 2, a: 2, b: 25, y: 5.857142857142858 } 41 | ---------------------------------- 42 | ``` 43 | 44 | ![Interpreter Demo](https://user-images.githubusercontent.com/3625244/28240074-1186c6d0-6983-11e7-97ab-974c81d07fb9.gif) 45 | 46 | ## Known issues 47 | 48 | - Procedures declarations are supported by parser and AST, but interpreter doesn't support its execution (there is no implementation for this at all); 49 | - There are no builtin IO procedures, like writing into standard output and reading from standard input; 50 | 51 | ## Phases 52 | 53 | This interpreter has several phases to execute before interpreting the source code: 54 | 55 | - [Lexical Analysis](./src/lexer) 56 | - [Syntax Analysis](./src/parser) 57 | - [Semantic Analysis](./src/semantic) 58 | - [Interpretation](./src/interpreter) 59 | 60 | Each of these steps are explained a little bit below. 61 | 62 | ### Lexical analysis 63 | 64 | > Lexical analysis is the process of converting a sequence of characters (such as in a computer program or web page) into a sequence of tokens (strings with an assigned and thus identified meaning) 65 | 66 | Implementation of scanner here is based on simple lookahead pointer in input. 67 | 68 | Scanner stores three properties: `input`, `position` and `currentChar`. 69 | 70 | - input stores the whole code of input source code; 71 | - position stores an integer which determines current position of a scanner in provided input; 72 | - currentChar stores a character stored by current position of a marker; 73 | 74 | Each time, when `advance()` called, it increments the value of `pointer` and read new character into `currentChar` property. 75 | 76 | Each time, when `getNextToken()` is called, it calls `advance()` as many times as soon the next valid token is meet the requirements. 77 | 78 | In a result, scanner returns a stream of a tokens. 79 | In our case, token is simply a structure with two fields: `type` and `value`. 80 | `type` recognizes a lexeme by its type, like NUMBER or SEMICOLON and `value` stores the original character sequence. 81 | 82 | ### Syntax analysis 83 | 84 | > Syntax analysis is the process of analysing a string of symbols, either in natural language or in computer languages, conforming to the rules of a formal grammar 85 | 86 | We are recognizing a string of tokens (stream of tokens) here. 87 | 88 | Parser implementation here has mapped all its methods onto formal grammar rules (productions). 89 | Each method has in its comments a production, that the method is trying to follow. 90 | 91 | I.e. take a look into _factor_ production: 92 | 93 | ```js 94 | /** 95 | * factor: PLUS factor 96 | * | MINUS factor 97 | * | INTEGER_LITERAL 98 | * | REAL_LITERAL 99 | * | LEFT_PARENTHESIS expr RIGHT_PARENTHESIS 100 | * | variable 101 | * 102 | * @returns {Node} 103 | */ 104 | factor() { 105 | const token = this.currentToken; 106 | 107 | if (token.is(Token.PLUS)) { 108 | this.eat(Token.PLUS); 109 | return AST.UnaryOperator.create(token, this.factor()); 110 | } else if (token.is(Token.MINUS)) { 111 | this.eat(Token.MINUS); 112 | return AST.UnaryOperator.create(token, this.factor()); 113 | } else if (token.is(Token.INTEGER_LITERAL)) { 114 | this.eat(Token.INTEGER_LITERAL); 115 | return AST.Number.create(token); 116 | } else if (token.is(Token.REAL_LITERAL)) { 117 | this.eat(Token.REAL_LITERAL); 118 | return AST.Number.create(token); 119 | } else if (token.is(Token.LEFT_PARENTHESIS)) { 120 | this.eat(Token.LEFT_PARENTHESIS); 121 | const node = this.expr(); 122 | this.eat(Token.RIGHT_PARENTHESIS); 123 | return node; 124 | } 125 | 126 | return this.variable(); 127 | } 128 | ``` 129 | 130 | Comments above the method show a grammar rule. 131 | Method implementation itself follows this grammar rule. 132 | 133 | Each terminal is a token, while each non-terminal is another method in a parser. 134 | 135 | As a result, syntax analysis returns a generated AST of a Pascal program. 136 | 137 | ### Semantic analysis 138 | 139 | > Semantic analysis, also context sensitive analysis, is a process in compiler construction, usually after parsing, to gather necessary semantic information from the source code. It usually includes type checking, or makes sure a variable is declared before use which is impossible to describe in Extended Backus–Naur Form and thus not easily detected during parsing. 140 | 141 | That's why semantic analysis is a separate phase of a compiler. 142 | Parsing know nothing about context in which your source code will run. 143 | 144 | Semantic analyzer is implemented as AST visitor, which takes an AST from the parser and visits all the nodes. 145 | 146 | Current implementation of a semantic analysis is just about symbol tables and type checking. 147 | Each time when visitor visits a node with variable declaration, it creates a record about its scope, type and name in symbol table. Each time when a node is procedure declaration, it creates record about its scope, formal parameters list and a name in symbol table. 148 | 149 | Afterwards, semantic analyzer checks if you have any duplicate declarations or doesn't have those at all. 150 | 151 | ### Interpretation 152 | 153 | > An interpreter is a computer program that directly executes, i.e. performs, instructions written in a programming or scripting language, without previously compiling them into a machine language program 154 | 155 | In our case, our "computer program" that directly executes the code is written in JavaScript and uses NodeJS runtime for these purposes. 156 | 157 | Interpreter implemented as AST visitor as well. 158 | After successful semantic analysis, AST goes into interpreter. 159 | 160 | Interpreter visits each AST node recursively and calls appropriate JavaScript runtime methods to execute the code. 161 | 162 | As a result, we got our code written in Pascal run. 163 | 164 | ## Roadmap 165 | 166 | ### Lexer 167 | 168 | - Dictionary of tokens **[DONE]** 169 | - Structure with accessors which defines token **[DONE]** 170 | - Casting tokens into strings when printing **[DONE]** 171 | - Helper for checking token types via *is()* **[DONE]** 172 | - Lexical analyzer which returns next token each time you call *getNextToken()* **[DONE]** 173 | - Skip whitespaces at all **[DONE]** 174 | - Read digits as one token **[DONE]** 175 | - Read alphanumeric as IDENTIFIER tokens **[DONE]** 176 | - Add a dictionary of reserved words in a language with appropriate tokens in it **[DONE]** 177 | 178 | ### Parser 179 | 180 | - Consuming (eating) tokens from a lexer **[DONE]** 181 | - Productions for mathematical expressions **[DONE]** 182 | - Associativity and precedence **[DONE]** 183 | - Support for parenthesis **[DONE]** 184 | - Support for variable assignments **[DONE]** 185 | - Support for compounds of statements **[DONE]** 186 | - Building an AST nodes **[DONE]** 187 | 188 | ### AST 189 | 190 | - Basic AST Node class **[DONE]** 191 | - Number Node **[DONE]** 192 | - BinaryOperator Node **[DONE]** 193 | - UnaryOperator Node **[DONE]** 194 | - Assign Node **[DONE]** 195 | - Compound Node **[DONE]** 196 | - NoOperation Node **[DONE]** 197 | - Variable Node **[DONE]** 198 | 199 | ### Interpreter 200 | 201 | - *visit()* that calls visitors for each AST node, based on its type **[DONE]** 202 | - Visitor for Number Node **[DONE]** 203 | - Visitor for UnaryOperator Node **[DONE]** 204 | - Visitor for BinaryOperator Node **[DONE]** 205 | - Visitor for NoOperation Node **[DONE]** 206 | - Visitor for Compound Node **[DONE]** 207 | - Visitor for Assign Node **[DONE]** 208 | - Visitor for Variable Node **[DONE]** 209 | 210 | ## License 211 | 212 | [MIT](./LICENSE) 213 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pascal-interpreter", 3 | "version": "1.0.0", 4 | "description": "A simple interpreter for a large subset of Pascal language written for educational purposes", 5 | "license": "MIT", 6 | "author": "ghaiklor", 7 | "main": "src/index.js", 8 | "homepage": "https://github.com/ghaiklor/pascal-interpreter#readme", 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/ghaiklor/pascal-interpreter.git" 12 | }, 13 | "bugs": { 14 | "url": "https://github.com/ghaiklor/pascal-interpreter/issues" 15 | }, 16 | "scripts": { 17 | "coveralls": "cat ./coverage/lcov.info | coveralls", 18 | "lint": "eslint --ignore-path .gitignore .", 19 | "precommit": "npm run lint && npm run test", 20 | "test": "istanbul cover _mocha" 21 | }, 22 | "devDependencies": { 23 | "chai": "4.1.1", 24 | "coveralls": "3.1.1", 25 | "eslint": "6.7.2", 26 | "eslint-config-node": "3.0.1", 27 | "husky": "4.1.0", 28 | "istanbul": "0.4.5", 29 | "mocha": "10.1.0", 30 | "sinon": "9.0.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/ast/Assign.js: -------------------------------------------------------------------------------- 1 | const Node = require('./Node'); 2 | 3 | /** 4 | * Assign Node represents assign operation to some variable. 5 | * 6 | * @class 7 | * @since 1.0.0 8 | */ 9 | class Assign extends Node { 10 | /** 11 | * Creates new instance of Assign node. 12 | * This node contains three parts: variable, assignment token and an expression. 13 | * 14 | * @param {Variable} variable Variable node 15 | * @param {Token} token Assignment token 16 | * @param {Node} expression AST node which indicates an expression 17 | */ 18 | constructor(variable, token, expression) { 19 | super(token); 20 | 21 | this.variable = variable; 22 | this.expression = expression; 23 | } 24 | 25 | /** 26 | * Get variable node where result must be stored. 27 | * 28 | * @returns {Variable} 29 | */ 30 | getVariable() { 31 | return this.variable; 32 | } 33 | 34 | /** 35 | * Get AST node of an expression which must be stored in a variable. 36 | * 37 | * @returns {Node} 38 | */ 39 | getExpression() { 40 | return this.expression; 41 | } 42 | 43 | /** 44 | * Static helper for creating Assign node. 45 | * 46 | * @static 47 | * @param {Variable} variable Variable node 48 | * @param {Token} token Assignment token 49 | * @param {Node} expression AST node which indicates an expression 50 | * @returns {Assign} Returns created Assign node 51 | */ 52 | static create(variable, token, expression) { 53 | return new this(variable, token, expression); 54 | } 55 | } 56 | 57 | module.exports = Assign; 58 | -------------------------------------------------------------------------------- /src/ast/BinaryOperator.js: -------------------------------------------------------------------------------- 1 | const Node = require('./Node'); 2 | 3 | /** 4 | * Class that represents a binary operation in AST. 5 | * 6 | * @class 7 | * @since 1.0.0 8 | */ 9 | class BinaryOperator extends Node { 10 | /** 11 | * Creates an instance of binary operation in AST. 12 | * Binary operation means that it has two operands and one operator. 13 | * 14 | * @param {Node} lhs AST Node (left operand) 15 | * @param {Token} operator Operator represented as a {@link Token} 16 | * @param {Node} rhs AST Node (right operand) 17 | */ 18 | constructor(lhs, operator, rhs) { 19 | super(operator); 20 | 21 | this.lhs = lhs; 22 | this.rhs = rhs; 23 | } 24 | 25 | /** 26 | * Get left assignment node from this binary operation. 27 | * 28 | * @returns {Node} 29 | */ 30 | getLHS() { 31 | return this.lhs; 32 | } 33 | 34 | /** 35 | * Returns an operator in this binary expression. 36 | * 37 | * @returns {Token} 38 | */ 39 | getOperator() { 40 | return this.getToken(); 41 | } 42 | 43 | /** 44 | * Get right assignment node from this binary operation. 45 | * 46 | * @returns {Node} 47 | */ 48 | getRHS() { 49 | return this.rhs; 50 | } 51 | 52 | /** 53 | * Static helper for creating new binary nodes. 54 | * 55 | * @param {Node} lhs AST Node (left operand) 56 | * @param {Token} operator Operator represented as a {@link Token} 57 | * @param {Node} rhs AST Node (right operand) 58 | * @returns {BinaryOperator} 59 | */ 60 | static create(lhs, operator, rhs) { 61 | return new this(lhs, operator, rhs); 62 | } 63 | } 64 | 65 | module.exports = BinaryOperator; 66 | -------------------------------------------------------------------------------- /src/ast/Block.js: -------------------------------------------------------------------------------- 1 | const Node = require('./Node'); 2 | 3 | /** 4 | * Block node holds declarations and a compound statement. 5 | * 6 | * @class 7 | * @since 1.0.0 8 | */ 9 | class Block extends Node { 10 | /** 11 | * Creates new Block instance. 12 | * 13 | * @param {Array} declarations 14 | * @param {Compound} compound 15 | */ 16 | constructor(declarations, compound) { 17 | super(null); 18 | 19 | this.declarations = declarations; 20 | this.compound = compound; 21 | } 22 | 23 | /** 24 | * Get an array with declarations in a block. 25 | * 26 | * @returns {Array} 27 | */ 28 | getDeclarations() { 29 | return this.declarations; 30 | } 31 | 32 | /** 33 | * Get a compound node with a statements inside. 34 | * 35 | * @returns {Compound} 36 | */ 37 | getCompound() { 38 | return this.compound; 39 | } 40 | 41 | /** 42 | * Static helper for creating Block node. 43 | * 44 | * @static 45 | * @param {Array} declarations 46 | * @param {Compound} compound 47 | * @returns {Block} 48 | */ 49 | static create(declarations, compound) { 50 | return new this(declarations, compound); 51 | } 52 | } 53 | 54 | module.exports = Block; 55 | -------------------------------------------------------------------------------- /src/ast/Compound.js: -------------------------------------------------------------------------------- 1 | const Node = require('./Node'); 2 | 3 | /** 4 | * Compound Node represents a compound statement. 5 | * It contains a list of statement nodes in its children property. 6 | * 7 | * @class 8 | * @since 1.0.0 9 | */ 10 | class Compound extends Node { 11 | /** 12 | * Creates a new Compound instance. 13 | * It returns an empty Compound node which must be filled with its children. 14 | * Its children is usual AST nodes pushed via {@link Compound#append} method. 15 | * Usually, this AST node needs to store an array of AST nodes in some logical scope. 16 | * I.e. in this language it is used for storing statements in BEGIN END block. 17 | * 18 | * @example 19 | * const compound = Compound.create(); 20 | * 21 | * compound.append(node1); 22 | * compound.append(node2); 23 | */ 24 | constructor() { 25 | super(null); 26 | 27 | this.children = []; 28 | } 29 | 30 | /** 31 | * Get an array with its children. 32 | * 33 | * @returns {Array} 34 | */ 35 | getChildren() { 36 | return this.children; 37 | } 38 | 39 | /** 40 | * Appends new AST node into its children. 41 | * 42 | * @param {Node} node 43 | * @returns {Compound} 44 | */ 45 | append(node) { 46 | this.children.push(node); 47 | 48 | return this; 49 | } 50 | 51 | /** 52 | * Static helper for creating Compound node. 53 | * 54 | * @static 55 | * @returns {Compound} 56 | */ 57 | static create() { 58 | return new this(); 59 | } 60 | } 61 | 62 | module.exports = Compound; 63 | -------------------------------------------------------------------------------- /src/ast/NoOperation.js: -------------------------------------------------------------------------------- 1 | const Node = require('./Node'); 2 | 3 | /** 4 | * NoOperation node. 5 | * 6 | * @class 7 | * @since 1.0.0 8 | */ 9 | class NoOperation extends Node { 10 | /** 11 | * Creates a NoOperation node. 12 | * It used to indicate a valid AST node which doesn't have statements. 13 | * I.e. a block `BEGIN END` is a valid compound, though it has nothing inside. 14 | * So that, we are creating a NoOperation node in this case. 15 | */ 16 | constructor() { 17 | super(null); 18 | } 19 | 20 | /** 21 | * Static helper for creating NoOperation node. 22 | * 23 | * @static 24 | * @returns {NoOperation} 25 | */ 26 | static create() { 27 | return new this(); 28 | } 29 | } 30 | 31 | module.exports = NoOperation; 32 | -------------------------------------------------------------------------------- /src/ast/Node.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Basic Node for AST. 3 | * 4 | * @class 5 | * @since 1.0.0 6 | */ 7 | class Node { 8 | /** 9 | * Creates an instance of Node. 10 | * Each node should have a {@link Token} instance to identify where from this node came. 11 | * In case, token is not relevant here, i.e. {@link Compound} node, you can set `null`. 12 | * That's because some of AST nodes contain other nodes only and not assigned to some char in input. 13 | * 14 | * @param {Token} [token] {@link Token} instance 15 | * @example 16 | * const token = Token.create(Token.NUMBER, 1234); 17 | * const node = Node.create(token); 18 | */ 19 | constructor(token) { 20 | this.token = token; 21 | } 22 | 23 | /** 24 | * Returns a token instance assigned to this Node. 25 | * 26 | * @returns {Token} 27 | */ 28 | getToken() { 29 | return this.token; 30 | } 31 | 32 | /** 33 | * Static helper for creating a new Node instance. 34 | * 35 | * @static 36 | * @param {Token} token 37 | * @returns {Node} 38 | */ 39 | static create(token) { 40 | return new this(token); 41 | } 42 | } 43 | 44 | module.exports = Node; 45 | -------------------------------------------------------------------------------- /src/ast/Number.js: -------------------------------------------------------------------------------- 1 | const Node = require('./Node'); 2 | 3 | /** 4 | * Class for representing numbers in AST. 5 | * 6 | * @class 7 | * @since 1.0.0 8 | */ 9 | class Number extends Node { 10 | /** 11 | * Creates a new instance of Number Node. 12 | * That's just a thin wrapper around {@link Token} instance. 13 | * It was created just for case, if we will need more complex logic for get its value. 14 | * 15 | * @param {Token} token {@link Token} that represents a number 16 | * @example 17 | * const token = Token.create(Token.INTEGER, 500); 18 | * const node = Number.create(token); 19 | */ 20 | constructor(token) { 21 | super(token); 22 | 23 | this.value = token.getValue(); 24 | } 25 | 26 | /** 27 | * Get a value of a number as integer. 28 | * 29 | * @returns {Number} 30 | */ 31 | getValue() { 32 | return parseInt(this.value); 33 | } 34 | 35 | /** 36 | * Static helper for creating this node. 37 | * 38 | * @param {Token} token 39 | * @returns {Number} 40 | */ 41 | static create(token) { 42 | return new this(token); 43 | } 44 | } 45 | 46 | module.exports = Number; 47 | -------------------------------------------------------------------------------- /src/ast/Param.js: -------------------------------------------------------------------------------- 1 | const Node = require('./Node'); 2 | 3 | /** 4 | * AST node for formal parameters in a procedure declaration. 5 | * 6 | * @class 7 | * @since 1.0.0 8 | */ 9 | class Param extends Node { 10 | /** 11 | * Creates new instance of Param node. 12 | * 13 | * @param {Variable} variable 14 | * @param {Type} type 15 | */ 16 | constructor(variable, type) { 17 | super(null); 18 | 19 | this.variable = variable; 20 | this.type = type; 21 | } 22 | 23 | /** 24 | * Get variable of a param. 25 | * 26 | * @returns {Variable} 27 | */ 28 | getVariable() { 29 | return this.variable; 30 | } 31 | 32 | /** 33 | * Get type of a param. 34 | * 35 | * @returns {Type} 36 | */ 37 | getType() { 38 | return this.type; 39 | } 40 | 41 | /** 42 | * Static helper for creating new Param node. 43 | * 44 | * @static 45 | * @param {Variable} variable 46 | * @param {Type} type 47 | * @returns {Param} 48 | */ 49 | static create(variable, type) { 50 | return new this(variable, type); 51 | } 52 | } 53 | 54 | module.exports = Param; 55 | -------------------------------------------------------------------------------- /src/ast/ProcedureDecl.js: -------------------------------------------------------------------------------- 1 | const Node = require('./Node'); 2 | 3 | /** 4 | * AST node for representing procedure declarations. 5 | * 6 | * @class 7 | * @since 1.0.0 8 | */ 9 | class ProcedureDecl extends Node { 10 | /** 11 | * Creates new instance of procedure declaration node. 12 | * 13 | * @param {String} name Procedure name 14 | * @param {Array} params Array of Param nodes 15 | * @param {Block} block 16 | */ 17 | constructor(name, params, block) { 18 | super(null); 19 | 20 | this.name = name; 21 | this.params = params; 22 | this.block = block; 23 | } 24 | 25 | /** 26 | * Returns procedure name. 27 | * 28 | * @returns {String} 29 | */ 30 | getName() { 31 | return this.name; 32 | } 33 | 34 | /** 35 | * Get params of a procedure. 36 | * 37 | * @returns {Array} 38 | */ 39 | getParams() { 40 | return this.params; 41 | } 42 | 43 | /** 44 | * Returns {@link Block} node of a procedure. 45 | * 46 | * @returns {Block} 47 | */ 48 | getBlock() { 49 | return this.block; 50 | } 51 | 52 | /** 53 | * Static helper for creating new ProcedureDecl node. 54 | * 55 | * @static 56 | * @param {String} name Procedure name 57 | * @param {Array} params Array of Param nodes 58 | * @param {Block} block 59 | * @returns {ProcedureDecl} 60 | */ 61 | static create(name, params, block) { 62 | return new this(name, params, block); 63 | } 64 | } 65 | 66 | module.exports = ProcedureDecl; 67 | -------------------------------------------------------------------------------- /src/ast/Program.js: -------------------------------------------------------------------------------- 1 | const Node = require('./Node'); 2 | 3 | /** 4 | * AST Node that represents a program definition. 5 | * 6 | * @class 7 | * @since 1.0.0 8 | */ 9 | class Program extends Node { 10 | /** 11 | * Creates new Program instance. 12 | * 13 | * @param {String} name 14 | * @param {Block} block 15 | */ 16 | constructor(name, block) { 17 | super(null); 18 | 19 | this.name = name; 20 | this.block = block; 21 | } 22 | 23 | /** 24 | * Get a name of a program. 25 | * 26 | * @returns {String} 27 | */ 28 | getName() { 29 | return this.name; 30 | } 31 | 32 | /** 33 | * Get block of a program. 34 | * 35 | * @returns {Block} 36 | */ 37 | getBlock() { 38 | return this.block; 39 | } 40 | 41 | /** 42 | * Static helper for creating Program node. 43 | * 44 | * @static 45 | * @param {String} name 46 | * @param {Block} block 47 | * @returns {Program} 48 | */ 49 | static create(name, block) { 50 | return new this(name, block); 51 | } 52 | } 53 | 54 | module.exports = Program; 55 | -------------------------------------------------------------------------------- /src/ast/Type.js: -------------------------------------------------------------------------------- 1 | const Node = require('./Node'); 2 | 3 | /** 4 | * AST node that holds information about variable type. 5 | * 6 | * @class 7 | * @since 1.0.0 8 | */ 9 | class Type extends Node { 10 | /** 11 | * Creates new Type node instance. 12 | * 13 | * @param {Token} token 14 | */ 15 | constructor(token) { 16 | super(token); 17 | 18 | this.value = token.getValue(); 19 | } 20 | 21 | /** 22 | * Get a value that represents a type. 23 | * 24 | * @returns {String} 25 | */ 26 | getValue() { 27 | return this.value; 28 | } 29 | 30 | /** 31 | * Static helper for creating Type node. 32 | * 33 | * @static 34 | * @param {Token} token 35 | * @returns {Type} 36 | */ 37 | static create(token) { 38 | return new this(token); 39 | } 40 | } 41 | 42 | module.exports = Type; 43 | -------------------------------------------------------------------------------- /src/ast/UnaryOperator.js: -------------------------------------------------------------------------------- 1 | const Node = require('./Node'); 2 | 3 | /** 4 | * AST Node for representing unary operators. 5 | * 6 | * @class 7 | * @since 1.0.0 8 | */ 9 | class UnaryOperator extends Node { 10 | /** 11 | * Creates a new instance of Node for unary operator. 12 | * It consists of two parts: operator and operand. 13 | * Operator is just a token which represents an operation you need to do. 14 | * Operand represents an AST node to which operator should apply. 15 | * 16 | * @param {Token} operator Unary operator 17 | * @param {Node} operand AST Node as an operand 18 | */ 19 | constructor(operator, operand) { 20 | super(operator); 21 | 22 | this.operand = operand; 23 | } 24 | 25 | /** 26 | * Get an operator of this unary operation. 27 | * 28 | * @returns {Token} 29 | */ 30 | getOperator() { 31 | return this.getToken(); 32 | } 33 | 34 | /** 35 | * Get an operand of this unary expression. 36 | * 37 | * @returns {Node} 38 | */ 39 | getOperand() { 40 | return this.operand; 41 | } 42 | 43 | /** 44 | * Static helper for creating UnaryOperator nodes. 45 | * 46 | * @static 47 | * @param {Token} operator Unary operator 48 | * @param {Node} operand AST Node as an operand 49 | * @returns {UnaryOperator} 50 | */ 51 | static create(operator, operand) { 52 | return new this(operator, operand); 53 | } 54 | } 55 | 56 | module.exports = UnaryOperator; 57 | -------------------------------------------------------------------------------- /src/ast/VarDecl.js: -------------------------------------------------------------------------------- 1 | const Node = require('./Node'); 2 | 3 | /** 4 | * VarDecl node holds a variable declaration with its type. 5 | * 6 | * @class 7 | * @since 1.0.0 8 | */ 9 | class VarDecl extends Node { 10 | /** 11 | * Creates new VarDecl node. 12 | * 13 | * @param {Variable} variable 14 | * @param {Type} type 15 | */ 16 | constructor(variable, type) { 17 | super(null); 18 | 19 | this.variable = variable; 20 | this.type = type; 21 | } 22 | 23 | /** 24 | * Get a variable node from its declaration. 25 | * 26 | * @returns {Variable} 27 | */ 28 | getVariable() { 29 | return this.variable; 30 | } 31 | 32 | /** 33 | * Get a type node from its declaration. 34 | * 35 | * @returns {Type} 36 | */ 37 | getType() { 38 | return this.type; 39 | } 40 | 41 | /** 42 | * Static helper for creating new VarDecl node. 43 | * 44 | * @static 45 | * @param {Variable} variable 46 | * @param {Type} type 47 | * @returns {VarDecl} 48 | */ 49 | static create(variable, type) { 50 | return new this(variable, type); 51 | } 52 | } 53 | 54 | module.exports = VarDecl; 55 | -------------------------------------------------------------------------------- /src/ast/Variable.js: -------------------------------------------------------------------------------- 1 | const Node = require('./Node'); 2 | 3 | /** 4 | * Variable node represents a variable in AST. 5 | * 6 | * @class 7 | * @since 1.0.0 8 | */ 9 | class Variable extends Node { 10 | /** 11 | * Creates new instance of Variable node. 12 | * All it has is just a token of the identifier and its name. 13 | * 14 | * @param {Token} token Identifier token that represents a variable 15 | * @param {String} name Name of a variable 16 | */ 17 | constructor(token, name) { 18 | super(token); 19 | 20 | this.name = name; 21 | } 22 | 23 | /** 24 | * Get a name of variable. 25 | * 26 | * @returns {String} 27 | */ 28 | getName() { 29 | return this.name; 30 | } 31 | 32 | /** 33 | * Static helper for creating Variable node. 34 | * 35 | * @static 36 | * @param {Token} token Identifier token that represents a variable 37 | * @param {String} name Name of a variable 38 | * @returns {Variable} 39 | */ 40 | static create(token, name) { 41 | return new this(token, name); 42 | } 43 | } 44 | 45 | module.exports = Variable; 46 | -------------------------------------------------------------------------------- /src/ast/index.js: -------------------------------------------------------------------------------- 1 | const Assign = require('./Assign'); 2 | const BinaryOperator = require('./BinaryOperator'); 3 | const Block = require('./Block'); 4 | const Compound = require('./Compound'); 5 | const Node = require('./Node'); 6 | const NoOperation = require('./NoOperation'); 7 | const Number = require('./Number'); 8 | const Param = require('./Param'); 9 | const ProcedureDecl = require('./ProcedureDecl'); 10 | const Program = require('./Program'); 11 | const Type = require('./Type'); 12 | const UnaryOperator = require('./UnaryOperator'); 13 | const VarDecl = require('./VarDecl'); 14 | const Variable = require('./Variable'); 15 | 16 | module.exports = { 17 | Assign, 18 | BinaryOperator, 19 | Block, 20 | Compound, 21 | Node, 22 | NoOperation, 23 | Number, 24 | Param, 25 | ProcedureDecl, 26 | Program, 27 | Type, 28 | UnaryOperator, 29 | VarDecl, 30 | Variable 31 | }; 32 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const Interpreter = require('./interpreter'); 2 | 3 | module.exports = function (input) { 4 | const interpreter = new Interpreter(input); 5 | 6 | return interpreter.interpret(); 7 | }; 8 | -------------------------------------------------------------------------------- /src/interpreter/Interpreter.js: -------------------------------------------------------------------------------- 1 | const Token = require('../lexer/Token'); 2 | const Parser = require('../parser'); 3 | 4 | /** 5 | * This class responsible for interpreting generated AST by the parser. 6 | * 7 | * @class 8 | * @since 1.0.0 9 | */ 10 | class Interpreter { 11 | /** 12 | * Creates a new instance of interpreter. 13 | * As a dependency it creates {@link Parser} instance and provides it with a source program. 14 | * Afterwards, it calls {@link Parser#parse} method, getting an AST of the program. 15 | * In result, interpreter is able to interpret the program, since it has AST of it. 16 | * 17 | * @param {String} input Source code of a program 18 | * @example 19 | * const interpreter = new Interpreter('2 + 5'); 20 | * const result = interpreter.interpret(); 21 | */ 22 | constructor(input) { 23 | this.parser = new Parser(input); 24 | this.ast = this.parser.parse(); 25 | } 26 | 27 | /** 28 | * Visits a specified {@link Node}. 29 | * Visiting means calling a specific method, providing a node, based on its type. 30 | * This method must return some result, in our case, result of an operation based on node info. 31 | * I.e. node is BinaryOperator, then need to call {@link Interpreter#onBinaryOperator}. 32 | * Which results into recursively visiting LHS and RHS and executing an OPERATOR on them. 33 | * 34 | * @param {Node} node AST Node 35 | * @returns {*} Returns a result from a Node visitor 36 | * @example 37 | * const interpreter = new Interpreter('2 + 5'); 38 | * 39 | * // AST here is BinaryOperator with LHS = Number(2), OPERATOR = + and RHS = Number(5) 40 | * interpreter.visit(interpreter.ast); 41 | * // Calling `visit` with this AST as an argument leads to calling `onBinaryOperator` 42 | * // which recursively calls `onNumber`, gets its values and returning a result 43 | */ 44 | visit(node) { 45 | const visitor = this[`on${node.constructor.name}`]; 46 | 47 | return visitor.call(this, node); 48 | } 49 | 50 | /** 51 | * Visitor for {@link NoOperation} node. 52 | * Do nothing here and just return the node itself. 53 | * 54 | * @param {NoOperation} node 55 | * @returns {NoOperation} 56 | */ 57 | onNoOperation(node) { 58 | return node; 59 | } 60 | 61 | /** 62 | * Visitor for {@link Compound} node. 63 | * Interpreter for a compound nodes is a sequential visiting of all its children. 64 | * 65 | * @param {Compound} node 66 | */ 67 | onCompound(node) { 68 | return node.getChildren().forEach(child => this.visit(child)); 69 | } 70 | 71 | /** 72 | * Visitor for {@link Assign} node. 73 | * Each time, we see an assignment, we need to resolve a variable where to assign. 74 | * Also, we need to visit an expression, so we know what to assign into the variable. 75 | * 76 | * @param {Assign} node 77 | */ 78 | onAssign(node) { 79 | const variableName = node.getVariable().getName(); 80 | 81 | process.GLOBAL_SCOPE = Object.assign({}, process.GLOBAL_SCOPE); 82 | process.GLOBAL_SCOPE[variableName] = this.visit(node.getExpression()); 83 | } 84 | 85 | /** 86 | * Visitor for {@link Variable} node. 87 | * All it does is simply gets a name of a variable and tries to look up it in symbol table. 88 | * 89 | * @param {Variable} node 90 | */ 91 | onVariable(node) { 92 | const variableName = node.getName(); 93 | 94 | return process.GLOBAL_SCOPE[variableName]; 95 | } 96 | 97 | /** 98 | * Visitor for {@link Number} Node. 99 | * Since our Number node have a numeric value, we return its value. 100 | * 101 | * @param {Number} node 102 | * @returns {Number} 103 | */ 104 | onNumber(node) { 105 | return node.getValue(); 106 | } 107 | 108 | /** 109 | * Visitor for {@link UnaryOperator} Node. 110 | * Each time we see an unary operator, visit an operand and apply operator to it. 111 | * 112 | * @param {UnaryOperator} node 113 | * @returns {*} 114 | */ 115 | onUnaryOperator(node) { 116 | const operator = node.getOperator(); 117 | const operand = node.getOperand(); 118 | 119 | if (operator.is(Token.PLUS)) { 120 | return +this.visit(operand); 121 | } else if (operator.is(Token.MINUS)) { 122 | return -this.visit(operand); 123 | } 124 | } 125 | 126 | /** 127 | * Visitor for {@link BinaryOperator} Node. 128 | * First of all, we need to visit left and right sides. 129 | * Afterwards, apply an operator to these results. 130 | * 131 | * @param {BinaryOperator} node 132 | * @returns {*} 133 | */ 134 | onBinaryOperator(node) { 135 | const lhs = node.getLHS(); 136 | const operator = node.getOperator(); 137 | const rhs = node.getRHS(); 138 | 139 | if (operator.is(Token.PLUS)) { 140 | return this.visit(lhs) + this.visit(rhs); 141 | } else if (operator.is(Token.MINUS)) { 142 | return this.visit(lhs) - this.visit(rhs); 143 | } else if (operator.is(Token.ASTERISK)) { 144 | return this.visit(lhs) * this.visit(rhs); 145 | } else if (operator.is(Token.SLASH)) { 146 | return this.visit(lhs) / this.visit(rhs); 147 | } else if (operator.is(Token.INTEGER_DIV)) { 148 | return this.visit(lhs) / this.visit(rhs); 149 | } 150 | } 151 | 152 | /** 153 | * Visitor for {@link Program} node. 154 | * 155 | * @param {Program} node 156 | */ 157 | onProgram(node) { 158 | this.visit(node.getBlock()); 159 | } 160 | 161 | /** 162 | * Visitor for {@link Block} node. 163 | * 164 | * @param {Block} node 165 | */ 166 | onBlock(node) { 167 | node.getDeclarations().forEach(decl => this.visit(decl)); 168 | this.visit(node.getCompound()); 169 | } 170 | 171 | /** 172 | * Visitor for {@link VarDecl} node. 173 | * 174 | * @param {VarDecl} node 175 | * @returns {VarDecl} 176 | */ 177 | onVarDecl(node) { 178 | return node; 179 | } 180 | 181 | /** 182 | * Visitor for {@link Type} node. 183 | * 184 | * @param {Type} node 185 | */ 186 | onType(node) { 187 | return node; 188 | } 189 | 190 | /** 191 | * Visitor for {@link ProcedureDecl} node. 192 | * 193 | * @param {ProcedureDecl} node 194 | * @returns {ProcedureDecl} 195 | */ 196 | onProcedureDecl(node) { 197 | return node; 198 | } 199 | 200 | /** 201 | * Interprets an AST and returns a result. 202 | * 203 | * @returns {*} 204 | */ 205 | interpret() { 206 | return this.visit(this.ast); 207 | } 208 | } 209 | 210 | module.exports = Interpreter; 211 | -------------------------------------------------------------------------------- /src/interpreter/index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./Interpreter'); 2 | -------------------------------------------------------------------------------- /src/lexer/Lexer.js: -------------------------------------------------------------------------------- 1 | const Token = require('./Token'); 2 | 3 | /** 4 | * Lexer of a language. 5 | * 6 | * @class 7 | * @since 1.0.0 8 | */ 9 | class Lexer { 10 | /** 11 | * Creates a new instance of a lexer. 12 | * When instance created, you need to call {@link Lexer#getNextToken} for get a token. 13 | * Each time you call {@link Lexer#getNextToken} it returns next token from specified input. 14 | * 15 | * @param {String} input Source code of a program 16 | * @example 17 | * const lexer = new Lexer('2 + 5'); 18 | */ 19 | constructor(input) { 20 | this.input = input; 21 | this.position = 0; 22 | this.currentChar = this.input[this.position]; 23 | } 24 | 25 | /** 26 | * Lexer has a pointer that specifies where we are located right now in input. 27 | * This method moves this pointer by one, incrementing its value. 28 | * Afterwards, it reads a new char at new pointer's location and stores in `currentChar`. 29 | * In case, pointer is out-of-range (end of input) it assigns `null` to `currentChar`. 30 | * 31 | * @returns {Lexer} Returns current instance of the lexer 32 | * @example 33 | * const lexer = new Lexer('2 + 5'); // position = 0, currentChar = '2' 34 | * 35 | * lexer 36 | * .advance() // position = 1, currentChar = ' ' 37 | * .advance() // position = 2, currentChar = '+' 38 | * .advance() // position = 3, currentChar = ' ' 39 | * .advance() // position = 4, currentChar = '5' 40 | * .advance() // position = 5, currentChar = null 41 | * .advance() // position = 6, currentChar = null 42 | */ 43 | advance() { 44 | this.position += 1; 45 | 46 | if (this.position > this.input.length - 1) { 47 | this.currentChar = null; 48 | } else { 49 | this.currentChar = this.input[this.position]; 50 | } 51 | 52 | return this; 53 | } 54 | 55 | /** 56 | * Peeks a following character from the input without modifying the pointer. 57 | * The difference here between {@link Lexer#advance} is that this method is pure. 58 | * It helps differentiate between different tokens that start with the same character. 59 | * I.e. ':' and ':=' are different tokens, but we can't say that for sure until we see the next char. 60 | * 61 | * @returns {String} 62 | * @example 63 | * const lexer = new Lexer('2 + 5'); // pointer = 0, currentChar = '2' 64 | * 65 | * lexer 66 | * .peek() // pointer = 0, currentChar = '2', returns ' ' 67 | * .advance() // pointer = 1, currentChar = ' ' 68 | * .peek() // pointer = 1, currentChar = ' ', returns '+' 69 | */ 70 | peek() { 71 | const position = this.position + 1; 72 | 73 | if (position > this.input.length - 1) return null; 74 | 75 | return this.input[position]; 76 | } 77 | 78 | /** 79 | * Skips whitespaces in a source code. 80 | * While `currentChar` is a whitespace do {@link Lexer#advance}. 81 | * That way, we literally skips any char that doesn't make sense to us. 82 | * 83 | * @returns {Lexer} Returns current instance of the lexer 84 | */ 85 | skipWhitespace() { 86 | while (this.currentChar && /\s/.test(this.currentChar)) { 87 | this.advance(); 88 | } 89 | 90 | return this; 91 | } 92 | 93 | /** 94 | * Skips all the comments in a source code. 95 | * While `currentChar` is not a closing comments block, we advance the pointer. 96 | * The last one advance is for eating a curly brace itself. 97 | * 98 | * @returns {Lexer} 99 | */ 100 | skipComment() { 101 | while (this.currentChar && this.currentChar !== '}') { 102 | this.advance(); 103 | } 104 | 105 | return this.advance(); 106 | } 107 | 108 | /** 109 | * Parses an number from a source code. 110 | * While `currentChar` is a digit [0-9], add a char into the string stack. 111 | * Afterwards, when `currentChar` is not a digit anymore, parses an number from the stack. 112 | * 113 | * @returns {Number} 114 | */ 115 | number() { 116 | let number = ''; 117 | 118 | while (this.currentChar && /\d/.test(this.currentChar)) { 119 | number += this.currentChar; 120 | this.advance(); 121 | } 122 | 123 | if (this.currentChar === '.') { 124 | number += this.currentChar; 125 | this.advance(); 126 | 127 | while (this.currentChar && /\d/.test(this.currentChar)) { 128 | number += this.currentChar; 129 | this.advance(); 130 | } 131 | 132 | return Token.create(Token.REAL_LITERAL, parseFloat(number)); 133 | } 134 | 135 | return Token.create(Token.INTEGER_LITERAL, parseInt(number)); 136 | } 137 | 138 | /** 139 | * Parses a sequence of alphanumeric characters and returns a token. 140 | * In case, the sequence is reserved word, it returns a predefined token for this word. 141 | * Otherwise, it returns an IDENTIFIER token. 142 | * 143 | * @returns {Token} 144 | * @example 145 | * const lexer = new Lexer('BEGIN x END'); 146 | * 147 | * lexer.identifier(); // Token(BEGIN, BEGIN) 148 | * lexer.identifier(); // Token(IDENTIFIER, x) 149 | * lexer.identifier(); // Token(END, END) 150 | */ 151 | identifier() { 152 | let identifier = ''; 153 | 154 | while (this.currentChar && /[a-zA-Z0-9]/.test(this.currentChar)) { 155 | identifier += this.currentChar; 156 | this.advance(); 157 | } 158 | 159 | return Lexer.RESERVED_WORDS[identifier] || Token.create(Token.IDENTIFIER, identifier); 160 | } 161 | 162 | /** 163 | * Returns a next token in a source program. 164 | * Each time it sees a match from the source program, it wraps info into a {@link Token}. 165 | * It means, that it doesn't return all the tokens at once. 166 | * You need to call this method each time, you need to get next token from the input program. 167 | * 168 | * @returns {Token} 169 | * @example 170 | * const lexer = new Lexer('2 + 5'); 171 | * 172 | * lexer.getNextToken(); // Token(INTEGER, 2) 173 | * lexer.getNextToken(); // Token(PLUS, +) 174 | * lexer.getNextToken(); // Token(INTEGER, 5) 175 | * lexer.getNextToken(); // Token(EOF, null) 176 | * lexer.getNextToken(); // Token(EOF, null) 177 | */ 178 | getNextToken() { 179 | while (this.currentChar) { 180 | if (/\s/.test(this.currentChar)) { 181 | this.skipWhitespace(); 182 | continue; 183 | } 184 | 185 | if (this.currentChar === '{') { 186 | this.advance(); 187 | this.skipComment(); 188 | continue; 189 | } 190 | 191 | if (/\d/.test(this.currentChar)) { 192 | return this.number(); 193 | } 194 | 195 | if (/[a-zA-Z]/.test(this.currentChar)) { 196 | return this.identifier(); 197 | } 198 | 199 | if (this.currentChar === ':' && this.peek() === '=') { 200 | this.advance().advance(); 201 | return Token.create(Token.ASSIGN, ':='); 202 | } 203 | 204 | if (this.currentChar === ':') { 205 | this.advance(); 206 | return Token.create(Token.COLON, ':'); 207 | } 208 | 209 | if (this.currentChar === ',') { 210 | this.advance(); 211 | return Token.create(Token.COMMA, ','); 212 | } 213 | 214 | if (this.currentChar === ';') { 215 | this.advance(); 216 | return Token.create(Token.SEMICOLON, ';'); 217 | } 218 | 219 | if (this.currentChar === '.') { 220 | this.advance(); 221 | return Token.create(Token.DOT, '.'); 222 | } 223 | 224 | if (this.currentChar === '+') { 225 | this.advance(); 226 | return Token.create(Token.PLUS, '+'); 227 | } 228 | 229 | if (this.currentChar === '-') { 230 | this.advance(); 231 | return Token.create(Token.MINUS, '-'); 232 | } 233 | 234 | if (this.currentChar === '*') { 235 | this.advance(); 236 | return Token.create(Token.ASTERISK, '*'); 237 | } 238 | 239 | if (this.currentChar === '/') { 240 | this.advance(); 241 | return Token.create(Token.SLASH, '/'); 242 | } 243 | 244 | if (this.currentChar === '(') { 245 | this.advance(); 246 | return Token.create(Token.LEFT_PARENTHESIS, '('); 247 | } 248 | 249 | if (this.currentChar === ')') { 250 | this.advance(); 251 | return Token.create(Token.RIGHT_PARENTHESIS, ')'); 252 | } 253 | 254 | Lexer.error(`Unexpected character: ${this.currentChar}`); 255 | } 256 | 257 | return Token.create(Token.EOF, null); 258 | } 259 | 260 | /** 261 | * Throws an error in a lexer context. 262 | * 263 | * @static 264 | * @param {String} msg An error message 265 | */ 266 | static error(msg) { 267 | throw new Error(`[Lexer]\n${msg}`); 268 | } 269 | 270 | /** 271 | * Returns a dictionary of reserved words in this language. 272 | * In case, identifier exists in this dictionary, we need to return a token for this identifier. 273 | * Otherwise, we need to create a token IDENTIFIER and provide it with a name of identifier. 274 | * 275 | * @static 276 | * @returns {Object} 277 | */ 278 | static get RESERVED_WORDS() { 279 | return { 280 | PROGRAM: Token.create(Token.PROGRAM, 'PROGRAM'), 281 | VAR: Token.create(Token.VAR, 'VAR'), 282 | DIV: Token.create(Token.INTEGER_DIV, 'DIV'), 283 | INTEGER: Token.create(Token.INTEGER_TYPE, 'INTEGER_TYPE'), 284 | REAL: Token.create(Token.REAL_TYPE, 'REAL_TYPE'), 285 | BEGIN: Token.create(Token.BEGIN, 'BEGIN'), 286 | END: Token.create(Token.END, 'END'), 287 | PROCEDURE: Token.create(Token.PROCEDURE, 'PROCEDURE') 288 | } 289 | } 290 | } 291 | 292 | module.exports = Lexer; 293 | -------------------------------------------------------------------------------- /src/lexer/Token.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Represents a token in a source code of a program. 3 | * 4 | * @class 5 | * @since 1.0.0 6 | */ 7 | class Token { 8 | /** 9 | * Creates a new Token instance. 10 | * 11 | * @param {String} type Token type from {@link Token} static dictionary 12 | * @param {String} value Value of a token 13 | * @example 14 | * new Token(Token.INTEGER, '1234'); 15 | * new Token(Token.PLUS, '+'); 16 | * new Token(Token.INTEGER, '5678'); 17 | */ 18 | constructor(type, value) { 19 | this.type = type; 20 | this.value = value; 21 | } 22 | 23 | /** 24 | * Returns a type of a token. 25 | * 26 | * @returns {String} 27 | */ 28 | getType() { 29 | return this.type || null; 30 | } 31 | 32 | /** 33 | * Returns a value of a token. 34 | * 35 | * @returns {String} 36 | */ 37 | getValue() { 38 | return this.value || null; 39 | } 40 | 41 | /** 42 | * Check if specified token type is this token. 43 | * 44 | * @param {String} tokenType Token type from {@link Token} static dictionary 45 | * @returns {Boolean} Returns true if provided type is equal to type of this token 46 | * @example 47 | * const token = Token.create(Token.INTEGER, '234'); 48 | * 49 | * token.is(Token.INTEGER); // true 50 | * token.is(Token.ASTERISK); // false 51 | */ 52 | is(tokenType) { 53 | return this.getType() === tokenType; 54 | } 55 | 56 | /** 57 | * Converts a token into string representation. 58 | * It useful when you need to debug some tokens. 59 | * Instead of printing a token as object, it will print as a string. 60 | * Format of this string is following: Token(, ). 61 | * 62 | * @returns {String} Returns a string in format Token(, ) 63 | * @example 64 | * const token = Token.create(Token.INTEGER, '1234'); 65 | * 66 | * console.log(token); // Token(INTEGER, 1234) 67 | */ 68 | toString() { 69 | return `Token(${this.getType()}, ${this.getValue()})`; 70 | } 71 | 72 | /** 73 | * Creates a new Token instance. 74 | * 75 | * @static 76 | * @param {String} type Token type from {@link Token} static dictionary 77 | * @param {String} value Value of a token 78 | * @returns {Token} Returns instantiated instance of a Token 79 | * @example 80 | * Token.create(Token.INTEGER, 1234); 81 | * Token.create(Token.PLUS, '+'); 82 | * Token.create(Token.INTEGER, 5678); 83 | */ 84 | static create(type, value) { 85 | return new this(type, value); 86 | } 87 | 88 | /** 89 | * Returns a Token type for a plus symbol (+). 90 | * 91 | * @static 92 | * @returns {String} 93 | */ 94 | static get PLUS() { 95 | return 'PLUS'; 96 | } 97 | 98 | /** 99 | * Returns a Token type for a minus symbol (-). 100 | * 101 | * @static 102 | * @returns {String} 103 | */ 104 | static get MINUS() { 105 | return 'MINUS'; 106 | } 107 | 108 | /** 109 | * Returns a Token type for an asterisk symbol (*). 110 | * 111 | * @static 112 | * @returns {String} 113 | */ 114 | static get ASTERISK() { 115 | return 'ASTERISK'; 116 | } 117 | 118 | /** 119 | * Returns a Token type for a slash sign (/). 120 | * 121 | * @static 122 | * @returns {String} 123 | */ 124 | static get SLASH() { 125 | return 'SLASH'; 126 | } 127 | 128 | /** 129 | * Returns a Token type for a backslash sign (\). 130 | * 131 | * @static 132 | * @returns {String} 133 | */ 134 | static get BACKSLASH() { 135 | return 'BACKSLASH'; 136 | } 137 | 138 | /** 139 | * Returns a Token type for a comma symbol (,). 140 | * 141 | * @static 142 | * @returns {String} 143 | */ 144 | static get COMMA() { 145 | return 'COMMA'; 146 | } 147 | 148 | /** 149 | * Returns a Token type for a dot symbol (.). 150 | * 151 | * @static 152 | * @returns {String} 153 | */ 154 | static get DOT() { 155 | return 'DOT'; 156 | } 157 | 158 | /** 159 | * Returns a Token type for a colon symbol (:). 160 | * 161 | * @static 162 | * @returns {String} 163 | */ 164 | static get COLON() { 165 | return 'COLON'; 166 | } 167 | 168 | /** 169 | * Returns a Token type for a semicolon symbol (;). 170 | * 171 | * @static 172 | * @returns {String} 173 | */ 174 | static get SEMICOLON() { 175 | return 'SEMICOLON'; 176 | } 177 | 178 | /** 179 | * Returns a Token type for a left parenthesis "(". 180 | * 181 | * @static 182 | * @returns {String} 183 | */ 184 | static get LEFT_PARENTHESIS() { 185 | return 'LEFT_PARENTHESIS'; 186 | } 187 | 188 | /** 189 | * Returns a Token type for a right parenthesis ")". 190 | * 191 | * @static 192 | * @returns {String} 193 | */ 194 | static get RIGHT_PARENTHESIS() { 195 | return 'RIGHT_PARENTHESIS'; 196 | } 197 | 198 | /** 199 | * Returns a Token type for an ASSIGN sequence of chars (:=). 200 | * 201 | * @static 202 | * @returns {String} 203 | */ 204 | static get ASSIGN() { 205 | return 'ASSIGN'; 206 | } 207 | 208 | /** 209 | * Returns a Token type for end-of-file. 210 | * 211 | * @static 212 | * @returns {String} 213 | */ 214 | static get EOF() { 215 | return 'EOF'; 216 | } 217 | 218 | /** 219 | * Returns a Token type for a BEGIN keyword. 220 | * This token marks a beginning of a compound statement. 221 | * 222 | * @static 223 | * @returns {String} 224 | */ 225 | static get BEGIN() { 226 | return 'BEGIN'; 227 | } 228 | 229 | /** 230 | * Returns a Token type for an END keyword. 231 | * This token marks the end of a compound statement. 232 | * 233 | * @static 234 | * @returns {String} 235 | */ 236 | static get END() { 237 | return 'END'; 238 | } 239 | 240 | /** 241 | * Returns a Token type for identifiers in a program. 242 | * Valid identifier starts with an alphabetical character. 243 | * 244 | * @static 245 | * @returns {String} 246 | */ 247 | static get IDENTIFIER() { 248 | return 'IDENTIFIER'; 249 | } 250 | 251 | /** 252 | * Returns a Token type for a PROGRAM keyword. 253 | * 254 | * @static 255 | * @returns {String} 256 | */ 257 | static get PROGRAM() { 258 | return 'PROGRAM'; 259 | } 260 | 261 | /** 262 | * Returns a Token type for a VAR keyword. 263 | * 264 | * @static 265 | * @returns {String} 266 | */ 267 | static get VAR() { 268 | return 'VAR'; 269 | } 270 | 271 | /** 272 | * Returns a Token type for an INTEGER type. 273 | * 274 | * @static 275 | * @returns {String} 276 | */ 277 | static get INTEGER_TYPE() { 278 | return 'INTEGER_TYPE'; 279 | } 280 | 281 | /** 282 | * Returns a Token type for a REAL type. 283 | * 284 | * @static 285 | * @returns {String} 286 | */ 287 | static get REAL_TYPE() { 288 | return 'REAL_TYPE'; 289 | } 290 | 291 | /** 292 | * Returns a Token type for an integer literals. 293 | * 294 | * @static 295 | * @returns {String} 296 | */ 297 | static get INTEGER_LITERAL() { 298 | return 'INTEGER_LITERAL'; 299 | } 300 | 301 | /** 302 | * Returns a Token type for a real literals. 303 | * 304 | * @static 305 | * @returns {String} 306 | */ 307 | static get REAL_LITERAL() { 308 | return 'REAL_LITERAL'; 309 | } 310 | 311 | /** 312 | * Returns a Token type for integer division (DIV). 313 | * 314 | * @static 315 | * @returns {String} 316 | */ 317 | static get INTEGER_DIV() { 318 | return 'INTEGER_DIV'; 319 | } 320 | 321 | /** 322 | * Returns a Token type for float division (/). 323 | * 324 | * @static 325 | * @returns {String} 326 | */ 327 | static get REAL_DIV() { 328 | return 'REAL_DIV'; 329 | } 330 | 331 | /** 332 | * Returns a Token type for PROCEDURE keyword (PROCEDURE). 333 | * 334 | * @static 335 | * @returns {String} 336 | */ 337 | static get PROCEDURE() { 338 | return 'PROCEDURE'; 339 | } 340 | } 341 | 342 | module.exports = Token; 343 | -------------------------------------------------------------------------------- /src/lexer/index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./Lexer'); 2 | -------------------------------------------------------------------------------- /src/parser/Parser.js: -------------------------------------------------------------------------------- 1 | const Token = require('../lexer/Token'); 2 | const Lexer = require('../lexer'); 3 | const AST = require('../ast'); 4 | 5 | /** 6 | * Parser implementation for a language. 7 | * Converts stream of tokens into AST. 8 | * 9 | * @class 10 | * @since 1.0.0 11 | */ 12 | class Parser { 13 | /** 14 | * Creates new parser instance. 15 | * It accepts as an input source code of a program. 16 | * In result, it will parse it and return an AST of specified program. 17 | * As a dependency, it uses the lexer which returns stream of tokens. 18 | * 19 | * @param {String} input Source code of a program 20 | * @example 21 | * const parser = new Parser('2 + 5'); 22 | */ 23 | constructor(input) { 24 | this.lexer = new Lexer(input); 25 | this.currentToken = this.lexer.getNextToken(); 26 | } 27 | 28 | /** 29 | * Consumes one specified token type. 30 | * In case, token type is not equal to the current one, it throws an error. 31 | * When you are consuming a token you are not expecting, it means broken syntax structure. 32 | * 33 | * @param {String} tokenType Token type from {@link Token} dictionary 34 | * @returns {Parser} Returns current parser instance 35 | * @example 36 | * const parser = new Parser('2 + 5'); // currentToken = INTEGER 37 | * 38 | * parser 39 | * .eat(Token.INTEGER) // currentToken = PLUS 40 | * .eat(Token.PLUS) // currentToken = INTEGER 41 | * .eat(Token.PLUS) // throws an error, because currentToken = INTEGER 42 | */ 43 | eat(tokenType) { 44 | if (this.currentToken.is(tokenType)) { 45 | this.currentToken = this.lexer.getNextToken(); 46 | } else { 47 | Parser.error(`You provided unexpected token type "${tokenType}" while current token is ${this.currentToken}`); 48 | } 49 | 50 | return this; 51 | } 52 | 53 | /** 54 | * empty: 55 | * 56 | * @returns {NoOperation} 57 | */ 58 | empty() { 59 | return AST.NoOperation.create(); 60 | } 61 | 62 | /** 63 | * variable: IDENTIFIER 64 | * 65 | * @returns {Variable} 66 | */ 67 | variable() { 68 | const node = AST.Variable.create(this.currentToken, this.currentToken.getValue()); 69 | this.eat(Token.IDENTIFIER); 70 | return node; 71 | } 72 | 73 | /** 74 | * factor: PLUS factor 75 | * | MINUS factor 76 | * | INTEGER_LITERAL 77 | * | REAL_LITERAL 78 | * | LEFT_PARENTHESIS expr RIGHT_PARENTHESIS 79 | * | variable 80 | * 81 | * @returns {Node} 82 | */ 83 | factor() { 84 | const token = this.currentToken; 85 | 86 | if (token.is(Token.PLUS)) { 87 | this.eat(Token.PLUS); 88 | return AST.UnaryOperator.create(token, this.factor()); 89 | } else if (token.is(Token.MINUS)) { 90 | this.eat(Token.MINUS); 91 | return AST.UnaryOperator.create(token, this.factor()); 92 | } else if (token.is(Token.INTEGER_LITERAL)) { 93 | this.eat(Token.INTEGER_LITERAL); 94 | return AST.Number.create(token); 95 | } else if (token.is(Token.REAL_LITERAL)) { 96 | this.eat(Token.REAL_LITERAL); 97 | return AST.Number.create(token); 98 | } else if (token.is(Token.LEFT_PARENTHESIS)) { 99 | this.eat(Token.LEFT_PARENTHESIS); 100 | const node = this.expr(); 101 | this.eat(Token.RIGHT_PARENTHESIS); 102 | return node; 103 | } 104 | 105 | return this.variable(); 106 | } 107 | 108 | /** 109 | * term: factor ASTERISK term 110 | * | factor SLASH term 111 | * | factor REAL_DIV term 112 | * | factor 113 | * 114 | * @returns {Node} 115 | */ 116 | term() { 117 | let node = this.factor(); 118 | 119 | while ([Token.ASTERISK, Token.SLASH, Token.INTEGER_DIV].some(type => this.currentToken.is(type))) { 120 | const token = this.currentToken; 121 | 122 | if (token.is(Token.ASTERISK)) { 123 | this.eat(Token.ASTERISK); 124 | } else if (token.is(Token.SLASH)) { 125 | this.eat(Token.SLASH); 126 | } else { 127 | this.eat(Token.INTEGER_DIV); 128 | } 129 | 130 | node = AST.BinaryOperator.create(node, token, this.factor()); 131 | } 132 | 133 | return node; 134 | } 135 | 136 | /** 137 | * expr: term PLUS expr 138 | * | term MINUS expr 139 | * | term 140 | * 141 | * @returns {Node} 142 | */ 143 | expr() { 144 | let node = this.term(); 145 | 146 | while ([Token.PLUS, Token.MINUS].some(type => this.currentToken.is(type))) { 147 | const token = this.currentToken; 148 | 149 | if (token.is(Token.PLUS)) { 150 | this.eat(Token.PLUS); 151 | } else { 152 | this.eat(Token.MINUS); 153 | } 154 | 155 | node = AST.BinaryOperator.create(node, token, this.term()); 156 | } 157 | 158 | return node; 159 | } 160 | 161 | /** 162 | * assignmentStatement: variable ASSIGN expr 163 | * 164 | * @returns {Assign} 165 | */ 166 | assignmentStatement() { 167 | const variable = this.variable(); 168 | const token = this.currentToken; 169 | this.eat(Token.ASSIGN); 170 | const expression = this.expr(); 171 | 172 | return AST.Assign.create(variable, token, expression); 173 | } 174 | 175 | /** 176 | * compoundStatement: BEGIN statementList END 177 | * 178 | * @returns {Compound} 179 | */ 180 | compoundStatement() { 181 | this.eat(Token.BEGIN); 182 | const nodes = this.statementList(); 183 | this.eat(Token.END); 184 | 185 | const root = AST.Compound.create(); 186 | nodes.forEach(node => root.append(node)); 187 | 188 | return root; 189 | } 190 | 191 | /** 192 | * statement: compoundStatement 193 | * | assignmentStatement 194 | * | empty 195 | * 196 | * @returns {Node} 197 | */ 198 | statement() { 199 | let node; 200 | 201 | if (this.currentToken.is(Token.BEGIN)) { 202 | node = this.compoundStatement(); 203 | } else if (this.currentToken.is(Token.IDENTIFIER)) { 204 | node = this.assignmentStatement(); 205 | } else { 206 | node = this.empty(); 207 | } 208 | 209 | return node; 210 | } 211 | 212 | /** 213 | * statementList: statement 214 | * | statement SEMICOLON statementList 215 | * 216 | * @returns {Array} 217 | */ 218 | statementList() { 219 | const node = this.statement(); 220 | const nodes = [node]; 221 | 222 | while (this.currentToken.is(Token.SEMICOLON)) { 223 | this.eat(Token.SEMICOLON); 224 | nodes.push(this.statement()); 225 | } 226 | 227 | if (this.currentToken.is(Token.IDENTIFIER)) { 228 | Parser.error(`Unexpected identifier in "statementList" production: ${this.currentToken}`); 229 | } 230 | 231 | return nodes; 232 | } 233 | 234 | /** 235 | * typeSpec: INTEGER_TYPE 236 | * | REAL_TYPE 237 | * 238 | * @returns {Type} 239 | */ 240 | typeSpec() { 241 | const token = this.currentToken; 242 | 243 | if (token.is(Token.INTEGER_TYPE)) { 244 | this.eat(Token.INTEGER_TYPE); 245 | } else { 246 | this.eat(Token.REAL_TYPE); 247 | } 248 | 249 | return AST.Type.create(token); 250 | } 251 | 252 | /** 253 | * variableDeclaration: IDENTIFIER (COMMA IDENTIFIER)* COLON typeSpec 254 | * 255 | * @returns {Array} 256 | */ 257 | variableDeclaration() { 258 | const varNodes = [AST.Variable.create(this.currentToken, this.currentToken.getValue())]; 259 | this.eat(Token.IDENTIFIER); 260 | 261 | while (this.currentToken.is(Token.COMMA)) { 262 | this.eat(Token.COMMA); 263 | varNodes.push(AST.Variable.create(this.currentToken, this.currentToken.getValue())); 264 | this.eat(Token.IDENTIFIER); 265 | } 266 | 267 | this.eat(Token.COLON); 268 | 269 | const typeNode = this.typeSpec(); 270 | 271 | return varNodes.map(node => AST.VarDecl.create(node, typeNode)); 272 | } 273 | 274 | /** 275 | * formalParameters: ID (COMMA ID)* COLON type_spec 276 | * 277 | * @returns {Array} 278 | */ 279 | formalParameters() { 280 | const varNodes = [AST.Variable.create(this.currentToken, this.currentToken.getValue())]; 281 | this.eat(Token.IDENTIFIER); 282 | 283 | while (this.currentToken.is(Token.COMMA)) { 284 | this.eat(Token.COMMA); 285 | varNodes.push(AST.Variable.create(this.currentToken, this.currentToken.getValue())); 286 | this.eat(Token.IDENTIFIER); 287 | } 288 | 289 | this.eat(Token.COLON); 290 | 291 | const typeNode = this.typeSpec(); 292 | 293 | return varNodes.map(varNode => AST.Param.create(varNode, typeNode)); 294 | } 295 | 296 | /** 297 | * formalParameterList: formalParameters 298 | * | formalParameters SEMICOLON formalParameterList 299 | * 300 | * @returns {Array} 301 | */ 302 | formalParameterList() { 303 | let params = this.formalParameters(); 304 | 305 | while (this.currentToken.is(Token.SEMICOLON)) { 306 | this.eat(Token.SEMICOLON); 307 | params = params.concat(this.formalParameters()); 308 | } 309 | 310 | return params; 311 | } 312 | 313 | /** 314 | * declarations: VAR (variableDeclaration SEMICOLON)+ 315 | * | (PROCEDURE ID (LPAREN formalParameterList RPAREN)? SEMICOLON block SEMICOLON)* 316 | * | empty 317 | * 318 | * @returns {Array} 319 | */ 320 | declarations() { 321 | let declarations = []; 322 | let params = []; 323 | 324 | if (this.currentToken.is(Token.VAR)) { 325 | this.eat(Token.VAR); 326 | 327 | while (this.currentToken.is(Token.IDENTIFIER)) { 328 | const varDecl = this.variableDeclaration(); 329 | declarations = declarations.concat(varDecl); 330 | this.eat(Token.SEMICOLON); 331 | } 332 | } 333 | 334 | while (this.currentToken.is(Token.PROCEDURE)) { 335 | this.eat(Token.PROCEDURE); 336 | 337 | const procedureName = this.currentToken.getValue(); 338 | this.eat(Token.IDENTIFIER); 339 | 340 | if (this.currentToken.is(Token.LEFT_PARENTHESIS)) { 341 | this.eat(Token.LEFT_PARENTHESIS); 342 | params = this.formalParameterList(); 343 | this.eat(Token.RIGHT_PARENTHESIS); 344 | } 345 | 346 | this.eat(Token.SEMICOLON); 347 | const blockNode = this.block(); 348 | this.eat(Token.SEMICOLON); 349 | 350 | const procedureNode = AST.ProcedureDecl.create(procedureName, params, blockNode); 351 | declarations.push(procedureNode); 352 | } 353 | 354 | return declarations; 355 | } 356 | 357 | /** 358 | * block: declarations compoundStatement 359 | * 360 | * @returns {Block} 361 | */ 362 | block() { 363 | const declarations = this.declarations(); 364 | const compoundStatement = this.compoundStatement(); 365 | 366 | return AST.Block.create(declarations, compoundStatement); 367 | } 368 | 369 | /** 370 | * program: PROGRAM variable SEMICOLON block DOT 371 | * 372 | * @returns {Node} 373 | */ 374 | program() { 375 | this.eat(Token.PROGRAM); 376 | 377 | const variableNode = this.variable(); 378 | const programName = variableNode.getName(); 379 | 380 | this.eat(Token.SEMICOLON); 381 | 382 | const blockNode = this.block(); 383 | const programNode = AST.Program.create(programName, blockNode); 384 | this.eat(Token.DOT); 385 | 386 | return programNode; 387 | } 388 | 389 | /** 390 | * Parses an input source program and returns an AST. 391 | * It uses all the grammar rules above to parse tokens and build AST from it. 392 | * 393 | * @returns {Node} 394 | * @example 395 | * const parser = new Parser('BEGIN END.'); 396 | * 397 | * parser.parse(); // return an object that represents an AST of source program 398 | */ 399 | parse() { 400 | return this.program(); 401 | } 402 | 403 | /** 404 | * Static helper for notifying about an error, during parsing. 405 | * 406 | * @static 407 | * @param {String} msg Error message 408 | */ 409 | static error(msg) { 410 | throw new Error(`[Parser]\n${msg}`); 411 | } 412 | } 413 | 414 | module.exports = Parser; 415 | -------------------------------------------------------------------------------- /src/parser/index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./Parser'); 2 | -------------------------------------------------------------------------------- /src/semantic/SemanticAnalyzer.js: -------------------------------------------------------------------------------- 1 | const SymbolTable = require('../symbols/SymbolTable'); 2 | const VariableSymbol = require('../symbols/VariableSymbol'); 3 | const ProcedureSymbol = require('../symbols/ProcedureSymbol'); 4 | 5 | class SemanticAnalyzer { 6 | /** 7 | * Creates new instance of SemanticAnalyzer. 8 | */ 9 | constructor() { 10 | this.scope = null; 11 | } 12 | 13 | /** 14 | * Get and call a visitor for specified Node. 15 | * 16 | * @param {Node} node 17 | */ 18 | visit(node) { 19 | const visitor = this[`on${node.constructor.name}`]; 20 | 21 | return visitor.call(this, node); 22 | } 23 | 24 | /** 25 | * Visitor for Block node. 26 | * 27 | * @param {Block} node 28 | */ 29 | onBlock(node) { 30 | node.getDeclarations().forEach(node => this.visit(node)); 31 | this.visit(node.getCompound()); 32 | } 33 | 34 | /** 35 | * Visitor for Program node. 36 | * 37 | * @param {Program} node 38 | */ 39 | onProgram(node) { 40 | this.scope = SymbolTable.create('global', 1, this.scope); 41 | this.visit(node.getBlock()); 42 | this.scope = this.scope.enclosingScope; 43 | } 44 | 45 | /** 46 | * Visitor for ProcedureDecl node. 47 | * 48 | * @param {ProcedureDecl} node 49 | */ 50 | onProcedureDecl(node) { 51 | const procedureName = node.getName(); 52 | const procedureSymbol = ProcedureSymbol.create(procedureName); 53 | 54 | this.scope.define(procedureSymbol); 55 | this.scope = SymbolTable.create(procedureName, this.scope.scopeLevel + 1, this.scope); 56 | 57 | node.getParams().forEach(param => { 58 | const paramType = this.scope.lookup(param.getType().getValue()); 59 | const paramName = param.getVariable().getName(); 60 | const varSymbol = VariableSymbol.create(paramName, paramType); 61 | 62 | this.scope.define(varSymbol); 63 | procedureSymbol.params.push(varSymbol); 64 | }); 65 | 66 | this.visit(node.getBlock()); 67 | this.scope = this.scope.enclosingScope; 68 | } 69 | 70 | /** 71 | * Visitor for Compound node. 72 | * 73 | * @param {Compound} node 74 | */ 75 | onCompound(node) { 76 | node.getChildren().forEach(node => this.visit(node)); 77 | } 78 | 79 | /** 80 | * Visitor for NoOperation node. 81 | * 82 | * @param {NoOperation} node 83 | */ 84 | onNoOperation(node) { 85 | return node; 86 | } 87 | 88 | /** 89 | * Visitor for VarDecl node. 90 | * 91 | * @param {VarDecl} node 92 | */ 93 | onVarDecl(node) { 94 | const typeName = node.getType().getValue(); 95 | const typeSymbol = this.scope.lookup(typeName); 96 | 97 | const varName = node.getVariable().getName(); 98 | const varSymbol = VariableSymbol.create(varName, typeSymbol); 99 | 100 | if (this.scope.lookup(varName, true)) throw new Error(`Duplicate declaration of ${varName}`); 101 | 102 | this.scope.define(varSymbol); 103 | } 104 | 105 | /** 106 | * Visitor for Variable node. 107 | * 108 | * @param {Variable} node 109 | */ 110 | onVariable(node) { 111 | const varName = node.getName(); 112 | const varSymbol = this.scope.lookup(varName); 113 | 114 | if (!varSymbol) throw new Error(`Variable ${varName} is not resolved`); 115 | } 116 | 117 | /** 118 | * Visitor for Assign node. 119 | * 120 | * @param {Assign} node 121 | */ 122 | onAssign(node) { 123 | this.visit(node.getExpression()); 124 | this.visit(node.getVariable()); 125 | } 126 | 127 | /** 128 | * Visitor for BinaryOperator node. 129 | * 130 | * @param {BinaryOperator} node 131 | */ 132 | onBinaryOperator(node) { 133 | this.visit(node.getLHS()); 134 | this.visit(node.getRHS()); 135 | } 136 | 137 | /** 138 | * Static helper for creating an instance. 139 | * 140 | * @static 141 | * @returns {SemanticAnalyzer} 142 | */ 143 | static create() { 144 | return new this(); 145 | } 146 | } 147 | 148 | module.exports = SemanticAnalyzer; 149 | -------------------------------------------------------------------------------- /src/symbols/ProcedureSymbol.js: -------------------------------------------------------------------------------- 1 | const Symbol = require('./Symbol'); 2 | 3 | /** 4 | * A symbol for procedure units in a program. 5 | * 6 | * @class 7 | * @since 1.0.0 8 | */ 9 | class ProcedureSymbol extends Symbol { 10 | /** 11 | * Creates new procedure symbol instance. 12 | * 13 | * @param {String} name 14 | * @param {Array} params 15 | */ 16 | constructor(name, params = []) { 17 | super(name); 18 | 19 | this.params = params; 20 | } 21 | 22 | /** 23 | * Get params assigned to this procedure symbol. 24 | * 25 | * @return {Array} 26 | */ 27 | getParams() { 28 | return this.params; 29 | } 30 | 31 | /** 32 | * Returns a string representation of a procedure symbol. 33 | * 34 | * @return {String} 35 | */ 36 | toString() { 37 | return `ProcedureSymbol(${this.name}, ${this.params})`; 38 | } 39 | 40 | /** 41 | * Static helper for creating procedure symbol. 42 | * 43 | * @static 44 | * @param {String} name 45 | * @param {Array} params 46 | * @return {ProcedureSymbol} 47 | */ 48 | static create(name, params) { 49 | return new this(name, params); 50 | } 51 | } 52 | 53 | module.exports = ProcedureSymbol; 54 | -------------------------------------------------------------------------------- /src/symbols/Symbol.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Base class for representing symbols in our program. 3 | * 4 | * @class 5 | * @since 1.0.0 6 | */ 7 | class Symbol { 8 | /** 9 | * Creates new Symbol instance. 10 | * Symbol instance has its name and an assigned type symbol. 11 | * Category of a symbol encoded in its class name itself. 12 | * 13 | * @param {String} name 14 | * @param {TypeSymbol} [type=null] 15 | */ 16 | constructor(name, type = null) { 17 | this.name = name; 18 | this.type = type; 19 | } 20 | 21 | /** 22 | * Get a name of the symbol. 23 | * 24 | * @returns {String} 25 | */ 26 | getName() { 27 | return this.name; 28 | } 29 | 30 | /** 31 | * Get a type of this symbol. 32 | * 33 | * @returns {TypeSymbol} 34 | */ 35 | getType() { 36 | return this.type; 37 | } 38 | 39 | /** 40 | * Helper for converting Symbol into string representation. 41 | * 42 | * @returns {String} 43 | */ 44 | toString() { 45 | return `Symbol(${this.getName()}, ${this.getType()})`; 46 | } 47 | 48 | /** 49 | * Static helper for creating new Symbol instance. 50 | * 51 | * @static 52 | * @param {String} name 53 | * @param {TypeSymbol} [type=null] 54 | * @returns {Symbol} 55 | */ 56 | static create(name, type) { 57 | return new this(name, type); 58 | } 59 | } 60 | 61 | module.exports = Symbol; 62 | -------------------------------------------------------------------------------- /src/symbols/SymbolTable.js: -------------------------------------------------------------------------------- 1 | const TypeSymbol = require('./TypeSymbol'); 2 | 3 | /** 4 | * Symbol table for storing all the symbols in a program. 5 | * 6 | * @class 7 | * @since 1.0.0 8 | */ 9 | class SymbolTable { 10 | /** 11 | * Creates new empty symbol table. 12 | * Symbol tables are used for storing and tracking various symbols in source code. 13 | * 14 | * @param {String} scopeName 15 | * @param {Number} scopeLevel 16 | * @param {SymbolTable} [enclosingScope=null] 17 | * @example 18 | * const table = SymbolTable.create(); 19 | * const symbol = VariableSymbol.create(); 20 | * 21 | * table.define(symbol); 22 | */ 23 | constructor(scopeName, scopeLevel, enclosingScope = null) { 24 | this.symbols = new Map(); 25 | this.scopeName = scopeName; 26 | this.scopeLevel = scopeLevel; 27 | this.enclosingScope = enclosingScope; 28 | 29 | this.initBuiltin(); 30 | } 31 | 32 | /** 33 | * Initialize the built-in types when the symbol table instance is created. 34 | * 35 | * @returns {SymbolTable} 36 | */ 37 | initBuiltin() { 38 | this.define(TypeSymbol.create('INTEGER')); 39 | this.define(TypeSymbol.create('REAL')); 40 | 41 | return this; 42 | } 43 | 44 | /** 45 | * Defines a symbol into symbol table. 46 | * 47 | * @param {Symbol} symbol 48 | * @returns {SymbolTable} 49 | */ 50 | define(symbol) { 51 | this.symbols.set(symbol.getName(), symbol); 52 | 53 | return this; 54 | } 55 | 56 | /** 57 | * Lookup for a symbol in symbol table by its name. 58 | * 59 | * @param {String} name 60 | * @param {Boolean} [currentScopeOnly=false] 61 | * @returns {Symbol} 62 | */ 63 | lookup(name, currentScopeOnly = false) { 64 | const symbol = this.symbols.get(name); 65 | 66 | if (symbol) return symbol; 67 | if (currentScopeOnly) return null; 68 | if (this.enclosingScope) return this.enclosingScope.lookup(name); 69 | } 70 | 71 | /** 72 | * Prints all the defined symbols in the current symbol table. 73 | * 74 | * @returns {String} 75 | */ 76 | toString() { 77 | let entries = ''; 78 | 79 | for (const [key, value] of this.symbols.entries()) { 80 | entries += `${key}:${value}\n`; 81 | } 82 | 83 | return `Scope Name: ${this.scopeName}\nScope Level: ${this.scopeLevel}\n\nEntries\n${entries}`; 84 | } 85 | 86 | /** 87 | * Static helper for creating new SymbolTable instance. 88 | * 89 | * @static 90 | * @param {String} scopeName 91 | * @param {Number} scopeLevel 92 | * @param {SymbolTable} [enclosingScope=null] 93 | * @returns {SymbolTable} 94 | */ 95 | static create(scopeName, scopeLevel, enclosingScope) { 96 | return new this(scopeName, scopeLevel, enclosingScope); 97 | } 98 | } 99 | 100 | module.exports = SymbolTable; 101 | -------------------------------------------------------------------------------- /src/symbols/SymbolTableBuilder.js: -------------------------------------------------------------------------------- 1 | const SymbolTable = require('./SymbolTable'); 2 | const VariableSymbol = require('./VariableSymbol'); 3 | 4 | /** 5 | * It's a specific class which constructs symbol table based on AST. 6 | * 7 | * @class 8 | * @since 1.0.0 9 | */ 10 | class SymbolTableBuilder { 11 | /** 12 | * Creates new instance of SymbolTableBuilder. 13 | */ 14 | constructor() { 15 | this.scope = SymbolTable.create(); 16 | } 17 | 18 | /** 19 | * Get a visitor for a node. 20 | * 21 | * @param {Node} node 22 | * @returns {*} 23 | */ 24 | visit(node) { 25 | const visitor = this[`on${node.constructor.name}`]; 26 | 27 | return visitor.call(this, node); 28 | } 29 | 30 | /** 31 | * Visitor for Block node. 32 | * 33 | * @param {Block} node 34 | */ 35 | onBlock(node) { 36 | node.getDeclarations().forEach(node => this.visit(node)); 37 | this.visit(node.getCompound()); 38 | } 39 | 40 | /** 41 | * Visitor for Program node. 42 | * 43 | * @param {Program} node 44 | */ 45 | onProgram(node) { 46 | this.visit(node.getBlock()); 47 | } 48 | 49 | /** 50 | * Visitor for BinaryOperation. 51 | * 52 | * @param {BinaryOperator} node 53 | */ 54 | onBinaryOperator(node) { 55 | this.visit(node.getLHS()); 56 | this.visit(node.getRHS()); 57 | } 58 | 59 | /** 60 | * Visitor for Number node. 61 | * 62 | * @param {Number} node 63 | * @returns {Number} 64 | */ 65 | onNumber(node) { 66 | return node; 67 | } 68 | 69 | /** 70 | * Visitor for UnaryOperator. 71 | * 72 | * @param {UnaryOperator} node 73 | */ 74 | onUnaryOperator(node) { 75 | this.visit(node.getOperand()); 76 | } 77 | 78 | /** 79 | * Visitor for Compound node. 80 | * 81 | * @param {Compound} node 82 | */ 83 | onCompound(node) { 84 | node.getChildren().forEach(node => this.visit(node)); 85 | } 86 | 87 | /** 88 | * Visitor for NoOperation node. 89 | * 90 | * @param {NoOperation} node 91 | * @returns {NoOperation}} 92 | */ 93 | onNoOperation(node) { 94 | return node; 95 | } 96 | 97 | /** 98 | * Visitor for VarDecl node. 99 | * It traverses the variable declaration and adds a symbol into symbol table. 100 | * 101 | * @param {VarDecl} node 102 | */ 103 | onVarDecl(node) { 104 | const typeName = node.getType().getValue(); 105 | const typeSymbol = this.scope.lookup(typeName); 106 | const varName = node.getVariable().getName(); 107 | const varSymbol = VariableSymbol.create(varName, typeSymbol); 108 | 109 | this.scope.define(varSymbol); 110 | } 111 | 112 | /** 113 | * Visitor for Assign node. 114 | * 115 | * @param {Assign} node 116 | */ 117 | onAssign(node) { 118 | const varName = node.getVariable().getName(); 119 | const varSymbol = this.scope.lookup(varName); 120 | 121 | if (!varSymbol) throw new Error(`Variable ${varName} is not declared`); 122 | 123 | return this.visit(node.getExpression()); 124 | } 125 | 126 | /** 127 | * Visitor for Variable node. 128 | * 129 | * @param {Variable} node 130 | */ 131 | onVariable(node) { 132 | const varName = node.getName(); 133 | const varSymbol = this.scope.lookup(varName); 134 | 135 | if (!varSymbol) throw new Error(`Variable ${varName} is not declared`); 136 | } 137 | 138 | /** 139 | * Visitor for ProcedureDecl node. 140 | * 141 | * @param {ProcedureDecl} node 142 | * @returns {ProcedureDecl} 143 | */ 144 | onProcedureDecl(node) { 145 | return node; 146 | } 147 | 148 | /** 149 | * Static helper for creating SymbolTableBuilder instance. 150 | * 151 | * @static 152 | * @returns {SymbolTableBuilder} 153 | */ 154 | static create() { 155 | return new this; 156 | } 157 | } 158 | 159 | module.exports = SymbolTableBuilder; 160 | -------------------------------------------------------------------------------- /src/symbols/TypeSymbol.js: -------------------------------------------------------------------------------- 1 | const Symbol = require('./Symbol'); 2 | 3 | /** 4 | * Symbol for representing type symbols in a program. 5 | * 6 | * @class 7 | * @since 1.0.0 8 | */ 9 | class TypeSymbol extends Symbol { 10 | /** 11 | * Helper for representing this type symbol as a string. 12 | * 13 | * @returns {String} 14 | */ 15 | toString() { 16 | return `TypeSymbol(${this.getName()})`; 17 | } 18 | 19 | /** 20 | * Static helper for creating new TypeSymbol. 21 | * 22 | * @static 23 | * @param {String} name 24 | * @returns {TypeSymbol} 25 | */ 26 | static create(name) { 27 | return new this(name); 28 | } 29 | } 30 | 31 | module.exports = TypeSymbol; 32 | -------------------------------------------------------------------------------- /src/symbols/VariableSymbol.js: -------------------------------------------------------------------------------- 1 | const Symbol = require('./Symbol'); 2 | 3 | /** 4 | * Represents variable symbols in a program. 5 | * 6 | * @class 7 | * @since 1.0.0 8 | */ 9 | class VariableSymbol extends Symbol { 10 | /** 11 | * Helper for converting VariableSymbol into string representation. 12 | * 13 | * @returns {String} 14 | */ 15 | toString() { 16 | return `VariableSymbol(${this.getName()}, ${this.getType()})`; 17 | } 18 | 19 | /** 20 | * Static helper for creating new VariableSymbol instances. 21 | * 22 | * @static 23 | * @param {String} name 24 | * @param {TypeSymbol} type 25 | * @returns {VariableSymbol} 26 | */ 27 | static create(name, type) { 28 | return new this(name, type); 29 | } 30 | } 31 | 32 | module.exports = VariableSymbol; 33 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../.eslintrc", 3 | "env": { 4 | "mocha": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/Entry.test.js: -------------------------------------------------------------------------------- 1 | const {assert} = require('chai'); 2 | const interpret = require('../src'); 3 | 4 | describe('Module::Entry', () => { 5 | it('Should properly interpret the simplest program', () => { 6 | process.GLOBAL_SCOPE = {}; 7 | 8 | const program = ` 9 | PROGRAM myProgram; 10 | VAR 11 | number : INTEGER; 12 | a, b : INTEGER; 13 | y : REAL; 14 | 15 | BEGIN {myProgram} 16 | number := 2; 17 | a := number; 18 | b := 10 * a + 10 * a DIV 4; 19 | y := 20 / 7 + 3.14; 20 | END. {myProgram} 21 | `; 22 | 23 | interpret(program); 24 | assert.deepEqual(process.GLOBAL_SCOPE, {a: 2, b: 25, number: 2, y: 5.857142857142858}); 25 | }); 26 | 27 | it('Should properly interpret the complex program with all interpreters features', () => { 28 | process.GLOBAL_SCOPE = {}; 29 | 30 | const program = ` 31 | PROGRAM myProgram; 32 | VAR 33 | number : INTEGER; 34 | a, b : INTEGER; 35 | y : REAL; 36 | 37 | PROCEDURE Alpha(a: INTEGER); 38 | VAR 39 | number: INTEGER; 40 | BEGIN 41 | END; 42 | 43 | BEGIN {myProgram} 44 | number := 2; 45 | a := number; 46 | b := 10 * a + 10 * a DIV 4; 47 | y := 20 / 7 + 3.14; 48 | END. {myProgram} 49 | `; 50 | 51 | interpret(program); 52 | assert.deepEqual(process.GLOBAL_SCOPE, {a: 2, b: 25, number: 2, y: 5.857142857142858}); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /test/ast/Assign.test.js: -------------------------------------------------------------------------------- 1 | const {assert} = require('chai'); 2 | const Assign = require('../../src/ast/Assign'); 3 | const Variable = require('../../src/ast/Variable'); 4 | const Token = require('../../src/lexer/Token'); 5 | const Number = require('../../src/ast/Number'); 6 | 7 | const VARIABLE = Variable.create(Token.create(Token.IDENTIFIER, 'x'), 'x'); 8 | const TOKEN = Token.create(Token.ASSIGN, ':='); 9 | const NUMBER = Number.create(Token.create(Token.INTEGER, 500)); 10 | 11 | describe('AST::Assign', () => { 12 | it('Should properly create Assign node', () => { 13 | const assign = Assign.create(VARIABLE, TOKEN, NUMBER); 14 | 15 | assert.instanceOf(assign, Assign); 16 | assert.instanceOf(assign.variable, Variable); 17 | assert.instanceOf(assign.token, Token); 18 | assert.instanceOf(assign.expression, Number); 19 | }); 20 | 21 | it('Should properly return variable node', () => { 22 | const assign = Assign.create(VARIABLE, TOKEN, NUMBER); 23 | 24 | assert.instanceOf(assign, Assign); 25 | assert.instanceOf(assign.getVariable(), Variable); 26 | assert.instanceOf(assign.getVariable().getToken(), Token); 27 | assert.equal(assign.getVariable().getName(), 'x'); 28 | }); 29 | 30 | it('Should properly return expression node', () => { 31 | const assign = Assign.create(VARIABLE, TOKEN, NUMBER); 32 | 33 | assert.instanceOf(assign, Assign); 34 | assert.instanceOf(assign.getExpression(), Number); 35 | assert.instanceOf(assign.getExpression().getToken(), Token); 36 | assert.equal(assign.getExpression().getValue(), 500); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /test/ast/BinaryOperator.test.js: -------------------------------------------------------------------------------- 1 | const {assert} = require('chai'); 2 | const Token = require('../../src/lexer/Token'); 3 | const Number = require('../../src/ast/Number'); 4 | const BinaryOperator = require('../../src/ast/BinaryOperator'); 5 | 6 | const LHS = Number.create(Token.create(Token.INTEGER, 250)); 7 | const OPERATOR = Token.create(Token.PLUS, '+'); 8 | const RHS = Number.create(Token.create(Token.INTEGER, 500)); 9 | 10 | describe('AST::BinaryOperator', () => { 11 | it('Should properly instantiate', () => { 12 | const node = new BinaryOperator(LHS, OPERATOR, RHS); 13 | 14 | assert.instanceOf(node, BinaryOperator); 15 | assert.instanceOf(node.lhs, Number); 16 | assert.instanceOf(node.token, Token); 17 | assert.instanceOf(node.rhs, Number); 18 | }); 19 | 20 | it('Should properly return LHS', () => { 21 | const node = new BinaryOperator(LHS, OPERATOR, RHS); 22 | 23 | assert.instanceOf(node.getLHS(), Number); 24 | assert.equal(node.getLHS().getValue(), 250); 25 | }); 26 | 27 | it('Should properly return binary operator', () => { 28 | const node = new BinaryOperator(LHS, OPERATOR, RHS); 29 | 30 | assert.instanceOf(node.getOperator(), Token); 31 | assert.ok(node.getOperator().is(Token.PLUS)); 32 | assert.equal(node.getOperator().getType(), 'PLUS'); 33 | assert.equal(node.getOperator().getValue(), '+'); 34 | }); 35 | 36 | it('Should properly return RHS', () => { 37 | const node = new BinaryOperator(LHS, OPERATOR, RHS); 38 | 39 | assert.instanceOf(node.getRHS(), Number); 40 | assert.equal(node.getRHS().getValue(), 500); 41 | }); 42 | 43 | it('Should properly create instance via static helper', () => { 44 | const node = BinaryOperator.create(LHS, OPERATOR, RHS); 45 | 46 | assert.instanceOf(node, BinaryOperator); 47 | assert.instanceOf(node.lhs, Number); 48 | assert.instanceOf(node.token, Token); 49 | assert.instanceOf(node.rhs, Number); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /test/ast/Block.test.js: -------------------------------------------------------------------------------- 1 | const {assert} = require('chai'); 2 | const Block = require('../../src/ast/Block'); 3 | const Compound = require('../../src/ast/Compound'); 4 | 5 | describe('AST::Block', () => { 6 | it('Should properly instantiate', () => { 7 | const block = Block.create([], Compound.create()); 8 | 9 | assert.isArray(block.declarations); 10 | assert.instanceOf(block.compound, Compound); 11 | }); 12 | 13 | it('Should properly return declarations', () => { 14 | const block = Block.create([], Compound.create()); 15 | 16 | assert.isArray(block.getDeclarations()); 17 | }); 18 | 19 | it('Should properly return compound', () => { 20 | const block = Block.create([], Compound.create()); 21 | 22 | assert.instanceOf(block.getCompound(), Compound); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/ast/Compound.test.js: -------------------------------------------------------------------------------- 1 | const {assert} = require('chai'); 2 | const Compound = require('../../src/ast/Compound'); 3 | 4 | describe('AST::Compound', () => { 5 | it('Should properly create instance of a Compound node', () => { 6 | const compound = Compound.create(); 7 | 8 | assert.instanceOf(compound, Compound); 9 | assert.isArray(compound.children); 10 | assert.equal(compound.children.length, 0); 11 | }); 12 | 13 | it('Should properly return its children', () => { 14 | const compound = Compound.create(); 15 | 16 | assert.isArray(compound.getChildren()); 17 | assert.equal(compound.getChildren().length, 0); 18 | }); 19 | 20 | it('Should properly append new children into compound', () => { 21 | const compound = Compound.create(); 22 | 23 | assert.isArray(compound.getChildren()); 24 | assert.equal(compound.getChildren().length, 0); 25 | assert.instanceOf(compound.append(1), Compound); 26 | assert.equal(compound.getChildren().length, 1); 27 | assert.deepEqual(compound.getChildren(), [1]); 28 | assert.instanceOf(compound.append({}), Compound); 29 | assert.equal(compound.getChildren().length, 2); 30 | assert.deepEqual(compound.getChildren(), [1, {}]); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /test/ast/NoOperation.test.js: -------------------------------------------------------------------------------- 1 | const {assert} = require('chai'); 2 | const NoOperation = require('../../src/ast/NoOperation'); 3 | 4 | describe('AST::NoOperation', () => { 5 | it('Should properly create node', () => { 6 | const noop = NoOperation.create(); 7 | 8 | assert.instanceOf(noop, NoOperation); 9 | assert.isNull(noop.getToken()); 10 | }) 11 | }); 12 | -------------------------------------------------------------------------------- /test/ast/Node.test.js: -------------------------------------------------------------------------------- 1 | const {assert} = require('chai'); 2 | const Token = require('../../src/lexer/Token'); 3 | const Node = require('../../src/ast/Node'); 4 | 5 | const TOKEN = Token.create(Token.INTEGER_LITERAL, 200); 6 | 7 | describe('AST::Node', () => { 8 | it('Should properly instantiate', () => { 9 | const node = new Node(TOKEN); 10 | 11 | assert.instanceOf(node, Node); 12 | assert.instanceOf(node.token, Token); 13 | }); 14 | 15 | it('Should properly return a token from node', () => { 16 | const node = new Node(TOKEN); 17 | 18 | assert.instanceOf(node.getToken(), Token); 19 | assert.ok(node.getToken().is(Token.INTEGER_LITERAL)); 20 | assert.equal(node.getToken().getType(), Token.INTEGER_LITERAL); 21 | assert.equal(node.getToken().getType(), 'INTEGER_LITERAL'); 22 | assert.equal(node.getToken().getValue(), 200); 23 | }); 24 | 25 | it('Should properly instantiate from static create', () => { 26 | const node = Node.create(TOKEN); 27 | 28 | assert.instanceOf(node, Node); 29 | assert.instanceOf(node.token, Token); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /test/ast/Number.test.js: -------------------------------------------------------------------------------- 1 | const {assert} = require('chai'); 2 | const Token = require('../../src/lexer/Token'); 3 | const Number = require('../../src/ast/Number'); 4 | 5 | const TOKEN = Token.create(Token.INTEGER_LITERAL, 500); 6 | 7 | describe('AST::Number', () => { 8 | it('Should properly instantiate Number node', () => { 9 | const node = Number.create(TOKEN); 10 | 11 | assert.instanceOf(node, Number); 12 | assert.instanceOf(node.token, Token); 13 | assert.ok(node.token.is(Token.INTEGER_LITERAL)); 14 | assert.equal(node.value, 500); 15 | }); 16 | 17 | it('Should properly return a value from Number node', () => { 18 | const node = Number.create(TOKEN); 19 | 20 | assert.instanceOf(node, Number); 21 | assert.equal(node.getValue(), 500); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /test/ast/Param.test.js: -------------------------------------------------------------------------------- 1 | const {assert} = require('chai'); 2 | const Param = require('../../src/ast/Param'); 3 | const Variable = require('../../src/ast/Variable'); 4 | const Type = require('../../src/ast/Type'); 5 | const Token = require('../../src/lexer/Token'); 6 | 7 | describe('AST::Param', () => { 8 | it('Should properly create instance', () => { 9 | const param = Param.create(Variable.create(), Type.create(Token.create(Token.INTEGER_TYPE, 'INTEGER'))); 10 | 11 | assert.instanceOf(param, Param); 12 | assert.instanceOf(param.variable, Variable); 13 | assert.instanceOf(param.type, Type); 14 | }); 15 | 16 | it('Should properly get variable', () => { 17 | const param = Param.create(Variable.create(), Type.create(Token.create(Token.INTEGER_TYPE, 'INTEGER'))); 18 | 19 | assert.instanceOf(param, Param); 20 | assert.instanceOf(param.getVariable(), Variable); 21 | }); 22 | 23 | it('Should properly get type', () => { 24 | const param = Param.create(Variable.create(), Type.create(Token.create(Token.INTEGER_TYPE, 'INTEGER'))); 25 | 26 | assert.instanceOf(param, Param); 27 | assert.instanceOf(param.getType(), Type); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /test/ast/ProcedureDecl.test.js: -------------------------------------------------------------------------------- 1 | const {assert} = require('chai'); 2 | const ProcedureDecl = require('../../src/ast/ProcedureDecl'); 3 | const Block = require('../../src/ast/Block'); 4 | const Param = require('../../src/ast/Param'); 5 | 6 | const PARAMS = [Param.create()]; 7 | 8 | describe('AST::ProcedureDecl', () => { 9 | it('Should properly instantiate', () => { 10 | const node = ProcedureDecl.create('proc', PARAMS, Block.create()); 11 | 12 | assert.instanceOf(node, ProcedureDecl); 13 | assert.isString(node.name); 14 | assert.isArray(node.params); 15 | assert.instanceOf(node.params[0], Param); 16 | assert.instanceOf(node.block, Block); 17 | }); 18 | 19 | it('Should properly get procedure name', () => { 20 | const node = ProcedureDecl.create('proc', PARAMS, Block.create()); 21 | 22 | assert.instanceOf(node, ProcedureDecl); 23 | assert.equal(node.getName(), 'proc'); 24 | }); 25 | 26 | it('Should properly get params', () => { 27 | const node = ProcedureDecl.create('proc', PARAMS, Block.create()); 28 | 29 | assert.instanceOf(node, ProcedureDecl); 30 | assert.isArray(node.getParams()); 31 | assert.instanceOf(node.getParams()[0], Param); 32 | }); 33 | 34 | it('Should properly get block of the procedure', () => { 35 | const node = ProcedureDecl.create('proc', PARAMS, Block.create()); 36 | 37 | assert.instanceOf(node, ProcedureDecl); 38 | assert.instanceOf(node.getBlock(), Block); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /test/ast/Program.test.js: -------------------------------------------------------------------------------- 1 | const {assert} = require('chai'); 2 | const Program = require('../../src/ast/Program'); 3 | const Block = require('../../src/ast/Block'); 4 | 5 | describe('AST::Program', () => { 6 | it('Should properly instantiate', () => { 7 | const program = Program.create('name', Block.create()); 8 | 9 | assert.instanceOf(program, Program); 10 | assert.isString(program.name); 11 | assert.instanceOf(program.block, Block); 12 | }); 13 | 14 | it('Should properly get name of a program', () => { 15 | const program = Program.create('name', Block.create()); 16 | 17 | assert.equal(program.getName(), 'name'); 18 | }); 19 | 20 | it('Should properly get block of a program', () => { 21 | const program = Program.create('name', Block.create()); 22 | 23 | assert.instanceOf(program.getBlock(), Block); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /test/ast/Type.test.js: -------------------------------------------------------------------------------- 1 | const {assert} = require('chai'); 2 | const Type = require('../../src/ast/Type'); 3 | const Token = require('../../src/lexer/Token'); 4 | 5 | describe('AST::Type', () => { 6 | it('Should properly instantiate', () => { 7 | const type = Type.create(Token.create(Token.INTEGER, 'INTEGER')); 8 | 9 | assert.instanceOf(type, Type); 10 | assert.instanceOf(type.token, Token); 11 | assert.isString(type.value); 12 | }); 13 | 14 | it('Should properly return type value', () => { 15 | const type = Type.create(Token.create(Token.INTEGER, 'INTEGER')); 16 | 17 | assert.instanceOf(type, Type); 18 | assert.instanceOf(type.token, Token); 19 | assert.equal(type.getValue(), 'INTEGER'); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /test/ast/UnaryOperator.test.js: -------------------------------------------------------------------------------- 1 | const {assert} = require('chai'); 2 | const Token = require('../../src/lexer/Token'); 3 | const Number = require('../../src/ast/Number'); 4 | const UnaryOperator = require('../../src/ast/UnaryOperator'); 5 | 6 | const OPERATOR = Token.create(Token.MINUS, '-'); 7 | const OPERAND = Number.create(Token.create(Token.INTEGER, 500)); 8 | 9 | describe('AST::UnaryOperator', () => { 10 | it('Should properly instantiate', () => { 11 | const node = new UnaryOperator(OPERATOR, OPERAND); 12 | 13 | assert.instanceOf(node, UnaryOperator); 14 | assert.instanceOf(node.token, Token); 15 | assert.ok(node.token.is(Token.MINUS)); 16 | assert.instanceOf(node.operand, Number); 17 | }); 18 | 19 | it('Should properly return operator', () => { 20 | const node = new UnaryOperator(OPERATOR, OPERAND); 21 | 22 | assert.instanceOf(node.getOperator(), Token); 23 | assert.ok(node.getOperator().is(Token.MINUS)); 24 | assert.equal(node.getOperator().getType(), Token.MINUS); 25 | assert.equal(node.getOperator().getValue(), '-'); 26 | }); 27 | 28 | it('Should properly return operand', () => { 29 | const node = new UnaryOperator(OPERATOR, OPERAND); 30 | 31 | assert.instanceOf(node.getOperand(), Number); 32 | assert.equal(node.getOperand().getValue(), 500); 33 | }); 34 | 35 | it('Should properly create instance via static helper', () => { 36 | const node = UnaryOperator.create(OPERATOR, OPERAND); 37 | 38 | assert.instanceOf(node, UnaryOperator); 39 | assert.instanceOf(node.token, Token); 40 | assert.ok(node.token.is(Token.MINUS)); 41 | assert.instanceOf(node.operand, Number); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /test/ast/VarDecl.test.js: -------------------------------------------------------------------------------- 1 | const {assert} = require('chai'); 2 | const VarDecl = require('../../src/ast/VarDecl'); 3 | const Variable = require('../../src/ast/Variable'); 4 | const Type = require('../../src/ast/Type'); 5 | const Token = require('../../src/lexer/Token'); 6 | 7 | describe('AST::VarDecl', () => { 8 | it('Should properly instantiate', () => { 9 | const decl = VarDecl.create(Variable.create(Token.create(Token.IDENTIFIER, 'a'), 'a'), Type.create(Token.create(Token.INTEGER))); 10 | 11 | assert.instanceOf(decl, VarDecl); 12 | assert.instanceOf(decl.variable, Variable); 13 | assert.instanceOf(decl.type, Type); 14 | }); 15 | 16 | it('Should properly get variable node', () => { 17 | const decl = VarDecl.create(Variable.create(Token.create(Token.IDENTIFIER, 'a'), 'a'), Type.create(Token.create(Token.INTEGER))); 18 | 19 | assert.instanceOf(decl.getVariable(), Variable); 20 | }); 21 | 22 | it('Should properly get type node', () => { 23 | const decl = VarDecl.create(Variable.create(Token.create(Token.IDENTIFIER, 'a'), 'a'), Type.create(Token.create(Token.INTEGER))); 24 | 25 | assert.instanceOf(decl.getType(), Type); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /test/ast/Variable.test.js: -------------------------------------------------------------------------------- 1 | const {assert} = require('chai'); 2 | const Variable = require('../../src/ast/Variable'); 3 | const Token = require('../../src/lexer/Token'); 4 | 5 | const TOKEN = Token.create(Token.IDENTIFIER, 'x'); 6 | 7 | describe('AST::Variable', () => { 8 | it('Should properly create Variable node instance', () => { 9 | const variable = Variable.create(TOKEN, TOKEN.getValue()); 10 | 11 | assert.instanceOf(variable, Variable); 12 | assert.instanceOf(variable.getToken(), Token); 13 | assert.isString(variable.name); 14 | }); 15 | 16 | it('Should properly get a name of variable', () => { 17 | const variable = Variable.create(TOKEN, TOKEN.getValue()); 18 | 19 | assert.instanceOf(variable, Variable); 20 | assert.equal(variable.getName(), 'x'); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /test/ast/index.test.js: -------------------------------------------------------------------------------- 1 | const {assert} = require('chai'); 2 | const AST = require('../../src/ast'); 3 | 4 | describe('AST::EntryPoint', () => { 5 | it('Should properly export all the nodes', () => { 6 | assert.isFunction(AST.Assign); 7 | assert.isFunction(AST.BinaryOperator); 8 | assert.isFunction(AST.Block); 9 | assert.isFunction(AST.Compound); 10 | assert.isFunction(AST.Node); 11 | assert.isFunction(AST.NoOperation); 12 | assert.isFunction(AST.Number); 13 | assert.isFunction(AST.Param); 14 | assert.isFunction(AST.ProcedureDecl); 15 | assert.isFunction(AST.Program); 16 | assert.isFunction(AST.Type); 17 | assert.isFunction(AST.UnaryOperator); 18 | assert.isFunction(AST.VarDecl); 19 | assert.isFunction(AST.Variable); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /test/interpreter/Interpreter.test.js: -------------------------------------------------------------------------------- 1 | const {assert} = require('chai'); 2 | const Interpreter = require('../../src/interpreter'); 3 | const Token = require('../../src/lexer/Token'); 4 | const Parser = require('../../src/parser'); 5 | const AST = require('../../src/ast'); 6 | 7 | describe('Interpreter', () => { 8 | it('Should properly handle creating new instance', () => { 9 | const interpreter = new Interpreter('PROGRAM program; BEGIN END.'); 10 | 11 | assert.instanceOf(interpreter.parser, Parser); 12 | assert.instanceOf(interpreter.ast, AST.Program); 13 | }); 14 | 15 | it('Should properly get a visitor for specified node', () => { 16 | const number = AST.Number.create(Token.create(Token.INTEGER_LITERAL, 500)); 17 | const interpreter = new Interpreter('PROGRAM program; BEGIN END.'); 18 | 19 | assert.isNumber(interpreter.visit(number)); 20 | assert.equal(interpreter.visit(number), 500); 21 | }); 22 | 23 | it('Should properly interpret the simplest program', () => { 24 | process.GLOBAL_SCOPE = {}; 25 | const program = `PROGRAM program; BEGIN x:= -2; y:= -x; z:=(x - y + +8) / 4 * -1 END.`; 26 | const interpreter = new Interpreter(program); 27 | 28 | interpreter.interpret(); 29 | 30 | assert.deepEqual(process.GLOBAL_SCOPE, {x: -2, y: 2, z: -1}); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /test/lexer/Lexer.test.js: -------------------------------------------------------------------------------- 1 | const {assert} = require('chai'); 2 | const Token = require('../../src/lexer/Token'); 3 | const Lexer = require('../../src/lexer'); 4 | 5 | describe('Lexer', () => { 6 | it('Should properly instantiate an instance', () => { 7 | const lexer = new Lexer('2 + 3'); 8 | 9 | assert.instanceOf(lexer, Lexer); 10 | assert.equal(lexer.input, '2 + 3'); 11 | assert.equal(lexer.position, 0); 12 | assert.equal(lexer.currentChar, '2'); 13 | }); 14 | 15 | it('Should properly advance a pointer', () => { 16 | const lexer = new Lexer('2 + 3'); 17 | 18 | assert.instanceOf(lexer, Lexer); 19 | assert.instanceOf(lexer.advance(), Lexer); 20 | assert.equal(lexer.input, '2 + 3'); 21 | assert.equal(lexer.position, 1); 22 | assert.equal(lexer.currentChar, ' '); 23 | assert.instanceOf(lexer.advance(), Lexer); 24 | assert.equal(lexer.input, '2 + 3'); 25 | assert.equal(lexer.position, 2); 26 | assert.equal(lexer.currentChar, '+'); 27 | }); 28 | 29 | it('Should properly peek a character without modifying a pointer', () => { 30 | const lexer = new Lexer('2 + 3'); 31 | 32 | assert.instanceOf(lexer, Lexer); 33 | assert.equal(lexer.input, '2 + 3'); 34 | assert.equal(lexer.position, 0); 35 | assert.equal(lexer.currentChar, '2'); 36 | assert.instanceOf(lexer.advance(), Lexer); 37 | assert.equal(lexer.input, '2 + 3'); 38 | assert.equal(lexer.position, 1); 39 | assert.equal(lexer.currentChar, ' '); 40 | assert.equal(lexer.peek(), '+'); 41 | assert.equal(lexer.input, '2 + 3'); 42 | assert.equal(lexer.position, 1); 43 | assert.equal(lexer.currentChar, ' '); 44 | assert.instanceOf(lexer.advance(), Lexer); 45 | assert.equal(lexer.input, '2 + 3'); 46 | assert.equal(lexer.position, 2); 47 | assert.equal(lexer.currentChar, '+'); 48 | assert.instanceOf(lexer.advance(), Lexer); 49 | assert.equal(lexer.input, '2 + 3'); 50 | assert.equal(lexer.position, 3); 51 | assert.equal(lexer.currentChar, ' '); 52 | assert.equal(lexer.peek(), '3'); 53 | assert.equal(lexer.input, '2 + 3'); 54 | assert.equal(lexer.position, 3); 55 | assert.equal(lexer.currentChar, ' '); 56 | assert.instanceOf(lexer.advance(), Lexer); 57 | assert.equal(lexer.input, '2 + 3'); 58 | assert.equal(lexer.position, 4); 59 | assert.equal(lexer.currentChar, '3'); 60 | assert.isNull(lexer.peek()); 61 | }); 62 | 63 | it('Should properly skip whitespaces in an input', () => { 64 | const lexer = new Lexer('2 + 3'); 65 | 66 | assert.instanceOf(lexer, Lexer); 67 | assert.equal(lexer.input, '2 + 3'); 68 | assert.equal(lexer.position, 0); 69 | assert.equal(lexer.currentChar, '2'); 70 | assert.instanceOf(lexer.skipWhitespace(), Lexer); 71 | assert.equal(lexer.input, '2 + 3'); 72 | assert.equal(lexer.position, 0); 73 | assert.equal(lexer.currentChar, '2'); 74 | assert.instanceOf(lexer.advance(), Lexer); 75 | assert.equal(lexer.input, '2 + 3'); 76 | assert.equal(lexer.position, 1); 77 | assert.equal(lexer.currentChar, ' '); 78 | assert.instanceOf(lexer.skipWhitespace(), Lexer); 79 | assert.equal(lexer.input, '2 + 3'); 80 | assert.equal(lexer.position, 5); 81 | assert.equal(lexer.currentChar, '+'); 82 | assert.instanceOf(lexer.advance(), Lexer); 83 | assert.equal(lexer.input, '2 + 3'); 84 | assert.equal(lexer.position, 6); 85 | assert.equal(lexer.currentChar, ' '); 86 | assert.instanceOf(lexer.skipWhitespace(), Lexer); 87 | assert.equal(lexer.input, '2 + 3'); 88 | assert.equal(lexer.position, 7); 89 | assert.equal(lexer.currentChar, '3'); 90 | }); 91 | 92 | it('Should properly skip comments in an input', () => { 93 | const lexer = new Lexer('{2 +} 3'); 94 | 95 | assert.instanceOf(lexer, Lexer); 96 | assert.equal(lexer.input, '{2 +} 3'); 97 | assert.equal(lexer.position, 0); 98 | assert.equal(lexer.currentChar, '{'); 99 | assert.instanceOf(lexer.skipComment(), Lexer); 100 | assert.equal(lexer.input, '{2 +} 3'); 101 | assert.equal(lexer.position, 5); 102 | assert.equal(lexer.currentChar, ' '); 103 | assert.instanceOf(lexer.advance(), Lexer); 104 | assert.equal(lexer.input, '{2 +} 3'); 105 | assert.equal(lexer.position, 6); 106 | assert.equal(lexer.currentChar, '3'); 107 | }); 108 | 109 | it('Should properly parse an integer number from an input', () => { 110 | const lexer = new Lexer('256 + 3'); 111 | 112 | assert.instanceOf(lexer, Lexer); 113 | assert.equal(lexer.input, '256 + 3'); 114 | assert.equal(lexer.position, 0); 115 | assert.equal(lexer.currentChar, '2'); 116 | assert.equal(lexer.number().getValue(), 256); 117 | assert.equal(lexer.input, '256 + 3'); 118 | assert.equal(lexer.position, 3); 119 | assert.equal(lexer.currentChar, ' '); 120 | assert.instanceOf(lexer.advance(), Lexer); 121 | assert.instanceOf(lexer.advance(), Lexer); 122 | assert.instanceOf(lexer.advance(), Lexer); 123 | assert.equal(lexer.input, '256 + 3'); 124 | assert.equal(lexer.position, 6); 125 | assert.equal(lexer.currentChar, '3'); 126 | assert.equal(lexer.number().getValue(), 3); 127 | }); 128 | 129 | it('Should properly parse a float number from an input', () => { 130 | const lexer = new Lexer('3.14'); 131 | 132 | assert.instanceOf(lexer, Lexer); 133 | assert.equal(lexer.input, '3.14'); 134 | assert.equal(lexer.position, 0); 135 | assert.equal(lexer.currentChar, '3'); 136 | assert.equal(lexer.number().getValue(), 3.14); 137 | assert.equal(lexer.input, '3.14'); 138 | assert.equal(lexer.position, 4); 139 | assert.equal(lexer.currentChar, null); 140 | }); 141 | 142 | it('Should properly skip comments', () => { 143 | const lexer = new Lexer('abc {comment}'); 144 | 145 | assert.instanceOf(lexer, Lexer); 146 | assert.ok(lexer.getNextToken().is(Token.IDENTIFIER)); 147 | assert.ok(lexer.getNextToken().is(Token.EOF)); 148 | }); 149 | 150 | it('Should properly parse an identifier from an input', () => { 151 | const lexer = new Lexer('BEGIN x y z END'); 152 | 153 | assert.instanceOf(lexer, Lexer); 154 | 155 | const begin = lexer.identifier(); 156 | assert.instanceOf(begin, Token); 157 | assert.ok(begin.is(Token.BEGIN)); 158 | assert.equal(begin.getType(), 'BEGIN'); 159 | assert.equal(begin.getValue(), 'BEGIN'); 160 | assert.instanceOf(lexer.advance(), Lexer); 161 | 162 | const x = lexer.identifier(); 163 | assert.instanceOf(x, Token); 164 | assert.ok(x.is(Token.IDENTIFIER)); 165 | assert.equal(x.getType(), 'IDENTIFIER'); 166 | assert.equal(x.getValue(), 'x'); 167 | assert.instanceOf(lexer.advance(), Lexer); 168 | 169 | const y = lexer.identifier(); 170 | assert.instanceOf(y, Token); 171 | assert.ok(y.is(Token.IDENTIFIER)); 172 | assert.equal(y.getType(), 'IDENTIFIER'); 173 | assert.equal(y.getValue(), 'y'); 174 | assert.instanceOf(lexer.advance(), Lexer); 175 | 176 | const z = lexer.identifier(); 177 | assert.instanceOf(z, Token); 178 | assert.ok(z.is(Token.IDENTIFIER)); 179 | assert.equal(z.getType(), 'IDENTIFIER'); 180 | assert.equal(z.getValue(), 'z'); 181 | assert.instanceOf(lexer.advance(), Lexer); 182 | 183 | const end = lexer.identifier(); 184 | assert.instanceOf(end, Token); 185 | assert.ok(end.is(Token.END)); 186 | assert.equal(end.getType(), 'END'); 187 | assert.equal(end.getValue(), 'END'); 188 | }); 189 | 190 | it('Should properly return a stream of tokens for +, -, *, /, : and ,', () => { 191 | const lexer = new Lexer('+ - * / : ,'); 192 | 193 | assert.equal(lexer.getNextToken(), 'Token(PLUS, +)'); 194 | assert.equal(lexer.getNextToken(), 'Token(MINUS, -)'); 195 | assert.equal(lexer.getNextToken(), 'Token(ASTERISK, *)'); 196 | assert.equal(lexer.getNextToken(), 'Token(SLASH, /)'); 197 | assert.equal(lexer.getNextToken(), 'Token(COLON, :)'); 198 | assert.equal(lexer.getNextToken(), 'Token(COMMA, ,)'); 199 | assert.equal(lexer.getNextToken(), 'Token(EOF, null)'); 200 | assert.equal(lexer.getNextToken(), 'Token(EOF, null)'); 201 | }); 202 | 203 | it('Should properly return a stream of tokens for integers', () => { 204 | const lexer = new Lexer('123 456 7890'); 205 | 206 | assert.equal(lexer.getNextToken(), 'Token(INTEGER_LITERAL, 123)'); 207 | assert.equal(lexer.getNextToken(), 'Token(INTEGER_LITERAL, 456)'); 208 | assert.equal(lexer.getNextToken(), 'Token(INTEGER_LITERAL, 7890)'); 209 | assert.equal(lexer.getNextToken(), 'Token(EOF, null)'); 210 | assert.equal(lexer.getNextToken(), 'Token(EOF, null)'); 211 | }); 212 | 213 | it('Should properly return a stream of tokens for floats', () => { 214 | const lexer = new Lexer('123 456.7890 555'); 215 | 216 | assert.equal(lexer.getNextToken(), 'Token(INTEGER_LITERAL, 123)'); 217 | assert.equal(lexer.getNextToken(), 'Token(REAL_LITERAL, 456.789)'); 218 | assert.equal(lexer.getNextToken(), 'Token(INTEGER_LITERAL, 555)'); 219 | assert.equal(lexer.getNextToken(), 'Token(EOF, null)'); 220 | assert.equal(lexer.getNextToken(), 'Token(EOF, null)'); 221 | }); 222 | 223 | it('Should properly return a stream of tokens for an expression with parenthesis', () => { 224 | const lexer = new Lexer('(1 + 2) * 3'); 225 | 226 | assert.equal(lexer.getNextToken(), 'Token(LEFT_PARENTHESIS, ()'); 227 | assert.equal(lexer.getNextToken(), 'Token(INTEGER_LITERAL, 1)'); 228 | assert.equal(lexer.getNextToken(), 'Token(PLUS, +)'); 229 | assert.equal(lexer.getNextToken(), 'Token(INTEGER_LITERAL, 2)'); 230 | assert.equal(lexer.getNextToken(), 'Token(RIGHT_PARENTHESIS, ))'); 231 | assert.equal(lexer.getNextToken(), 'Token(ASTERISK, *)'); 232 | assert.equal(lexer.getNextToken(), 'Token(INTEGER_LITERAL, 3)'); 233 | assert.equal(lexer.getNextToken(), 'Token(EOF, null)'); 234 | assert.equal(lexer.getNextToken(), 'Token(EOF, null)'); 235 | }); 236 | 237 | it('Should properly return a stream of tokens for a Pascal program definition', () => { 238 | const lexer = new Lexer('BEGIN a := 2; END.'); 239 | 240 | assert.equal(lexer.getNextToken(), 'Token(BEGIN, BEGIN)'); 241 | assert.equal(lexer.getNextToken(), 'Token(IDENTIFIER, a)'); 242 | assert.equal(lexer.getNextToken(), 'Token(ASSIGN, :=)'); 243 | assert.equal(lexer.getNextToken(), 'Token(INTEGER_LITERAL, 2)'); 244 | assert.equal(lexer.getNextToken(), 'Token(SEMICOLON, ;)'); 245 | assert.equal(lexer.getNextToken(), 'Token(END, END)'); 246 | assert.equal(lexer.getNextToken(), 'Token(DOT, .)'); 247 | assert.equal(lexer.getNextToken(), 'Token(EOF, null)'); 248 | }); 249 | 250 | it('Should properly throw an error if unknown character', () => { 251 | const lexer = new Lexer('~'); 252 | 253 | assert.throws(() => lexer.getNextToken(), `[Lexer]\nUnexpected character: ~`); 254 | }); 255 | 256 | it('Should properly throw an error during analysis', () => { 257 | const lexer = new Lexer('2 + ~'); 258 | 259 | assert.equal(lexer.getNextToken(), 'Token(INTEGER_LITERAL, 2)'); 260 | assert.equal(lexer.getNextToken(), 'Token(PLUS, +)'); 261 | assert.throws(() => lexer.getNextToken(), `[Lexer]\nUnexpected character: ~`); 262 | }); 263 | 264 | it('Should properly return a dictionary of reserved words in a language', () => { 265 | assert.instanceOf(Lexer.RESERVED_WORDS.PROGRAM, Token); 266 | assert.instanceOf(Lexer.RESERVED_WORDS.VAR, Token); 267 | assert.instanceOf(Lexer.RESERVED_WORDS.DIV, Token); 268 | assert.instanceOf(Lexer.RESERVED_WORDS.INTEGER, Token); 269 | assert.instanceOf(Lexer.RESERVED_WORDS.REAL, Token); 270 | assert.instanceOf(Lexer.RESERVED_WORDS.BEGIN, Token); 271 | assert.instanceOf(Lexer.RESERVED_WORDS.END, Token); 272 | assert.instanceOf(Lexer.RESERVED_WORDS.PROCEDURE, Token); 273 | }); 274 | }); 275 | -------------------------------------------------------------------------------- /test/lexer/Token.test.js: -------------------------------------------------------------------------------- 1 | const {assert} = require('chai'); 2 | const Token = require('../../src/lexer/Token'); 3 | 4 | describe('Lexer::Token', () => { 5 | it('Should properly create a Token instance', () => { 6 | const token = new Token(Token.INTEGER_LITERAL, '2'); 7 | 8 | assert.instanceOf(token, Token); 9 | assert.isString(token.type); 10 | assert.isString(token.value); 11 | assert.equal(token.type, 'INTEGER_LITERAL'); 12 | assert.equal(token.value, '2'); 13 | }); 14 | 15 | it('Should properly return a type of a token', () => { 16 | const token = new Token(Token.INTEGER_LITERAL, '2'); 17 | const token2 = new Token(); 18 | 19 | assert.equal(token.getType(), Token.INTEGER_LITERAL); 20 | assert.equal(token.getType(), 'INTEGER_LITERAL'); 21 | assert.isNull(token2.getType()); 22 | assert.equal(token2.getType(), null); 23 | }); 24 | 25 | it('Should properly return a value of a token', () => { 26 | const token = new Token(Token.INTEGER_LITERAL, '2'); 27 | const token2 = new Token(); 28 | 29 | assert.equal(token.getValue(), '2'); 30 | assert.isNull(token2.getValue()); 31 | assert.equal(token2.getValue(), null); 32 | }); 33 | 34 | it('Should properly check if token type is equal to specified', () => { 35 | const token = Token.create(Token.INTEGER_LITERAL, '234'); 36 | 37 | assert.ok(token.is(Token.INTEGER_LITERAL)); 38 | assert.notOk(token.is(Token.ASTERISK)); 39 | assert.notOk(token.is(Token.PLUS)); 40 | assert.notOk(token.is(null)); 41 | assert.notOk(token.is()); 42 | }); 43 | 44 | it('Should properly convert a token into string representation', () => { 45 | const token = new Token(Token.INTEGER_LITERAL, '2'); 46 | const token2 = new Token(Token.PLUS, '+'); 47 | const token3 = new Token(Token.BACKSLASH, 'bla bla'); 48 | const token4 = new Token(Token.MINUS); 49 | const token5 = new Token(); 50 | const token6 = new Token(null, 'value'); 51 | 52 | assert.equal(token.toString(), 'Token(INTEGER_LITERAL, 2)'); 53 | assert.equal(token2.toString(), 'Token(PLUS, +)'); 54 | assert.equal(token3.toString(), 'Token(BACKSLASH, bla bla)'); 55 | assert.equal(token4.toString(), 'Token(MINUS, null)'); 56 | assert.equal(token5.toString(), 'Token(null, null)'); 57 | assert.equal(token6.toString(), 'Token(null, value)'); 58 | }); 59 | 60 | it('Should properly create a new token via static create method', () => { 61 | const token = Token.create(Token.INTEGER_LITERAL, '2'); 62 | 63 | assert.instanceOf(token, Token); 64 | assert.ok(token.is(Token.INTEGER_LITERAL)); 65 | assert.equal(token.getType(), 'INTEGER_LITERAL'); 66 | assert.equal(token.getValue(), '2'); 67 | }); 68 | 69 | it('Should properly return a token types from static getters', () => { 70 | assert.equal(Token.PLUS, 'PLUS'); 71 | assert.equal(Token.MINUS, 'MINUS'); 72 | assert.equal(Token.ASTERISK, 'ASTERISK'); 73 | assert.equal(Token.SLASH, 'SLASH'); 74 | assert.equal(Token.BACKSLASH, 'BACKSLASH'); 75 | assert.equal(Token.COMMA, 'COMMA'); 76 | assert.equal(Token.DOT, 'DOT'); 77 | assert.equal(Token.COLON, 'COLON'); 78 | assert.equal(Token.SEMICOLON, 'SEMICOLON'); 79 | assert.equal(Token.LEFT_PARENTHESIS, 'LEFT_PARENTHESIS'); 80 | assert.equal(Token.RIGHT_PARENTHESIS, 'RIGHT_PARENTHESIS'); 81 | assert.equal(Token.ASSIGN, 'ASSIGN'); 82 | assert.equal(Token.EOF, 'EOF'); 83 | assert.equal(Token.BEGIN, 'BEGIN'); 84 | assert.equal(Token.END, 'END'); 85 | assert.equal(Token.IDENTIFIER, 'IDENTIFIER'); 86 | assert.equal(Token.PROGRAM, 'PROGRAM'); 87 | assert.equal(Token.VAR, 'VAR'); 88 | assert.equal(Token.INTEGER_TYPE, 'INTEGER_TYPE'); 89 | assert.equal(Token.REAL_TYPE, 'REAL_TYPE'); 90 | assert.equal(Token.INTEGER_LITERAL, 'INTEGER_LITERAL'); 91 | assert.equal(Token.REAL_LITERAL, 'REAL_LITERAL'); 92 | assert.equal(Token.INTEGER_DIV, 'INTEGER_DIV'); 93 | assert.equal(Token.REAL_DIV, 'REAL_DIV'); 94 | assert.equal(Token.PROCEDURE, 'PROCEDURE'); 95 | }); 96 | }); 97 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | test/**/*.test.js 2 | --reporter spec 3 | --recursive 4 | -------------------------------------------------------------------------------- /test/parser/Parser.test.js: -------------------------------------------------------------------------------- 1 | const {assert} = require('chai'); 2 | const Token = require('../../src/lexer/Token'); 3 | const Lexer = require('../../src/lexer'); 4 | const Parser = require('../../src/parser'); 5 | const AST = require('../../src/ast'); 6 | 7 | describe('Parser', () => { 8 | it('Should properly instantiate', () => { 9 | const parser = new Parser('2 + 5'); 10 | 11 | assert.instanceOf(parser.lexer, Lexer); 12 | assert.instanceOf(parser.currentToken, Token); 13 | }); 14 | 15 | it('Should properly consume (eat) a token', () => { 16 | const parser = new Parser('2 + 5'); 17 | 18 | assert.ok(parser.currentToken.is(Token.INTEGER_LITERAL)); 19 | assert.instanceOf(parser.eat(Token.INTEGER_LITERAL), Parser); 20 | assert.ok(parser.currentToken.is(Token.PLUS)); 21 | assert.instanceOf(parser.eat(Token.PLUS), Parser); 22 | assert.ok(parser.currentToken.is(Token.INTEGER_LITERAL)); 23 | assert.throws(() => parser.eat(Token.ASTERISK), Error, '[Parser]\nYou provided unexpected token type "ASTERISK" while current token is Token(INTEGER_LITERAL, 5)'); 24 | }); 25 | 26 | it('Should properly return NoOperation node for an empty production', () => { 27 | const parser = new Parser(''); 28 | 29 | assert.instanceOf(parser.empty(), AST.NoOperation); 30 | }); 31 | 32 | it('Should properly return Variable node from variable production', () => { 33 | const parser = new Parser('x y'); 34 | const x = parser.variable(); 35 | const y = parser.variable(); 36 | 37 | assert.instanceOf(x, AST.Variable); 38 | assert.instanceOf(x.getToken(), Token); 39 | assert.ok(x.getToken().is(Token.IDENTIFIER)); 40 | assert.equal(x.getName(), 'x'); 41 | assert.instanceOf(y, AST.Variable); 42 | assert.equal(y.getName(), 'y'); 43 | assert.instanceOf(y.getToken(), Token); 44 | assert.ok(y.getToken().is(Token.IDENTIFIER)); 45 | }); 46 | 47 | it('Should properly handle factor production', () => { 48 | const parser = new Parser('+1 -2 3 (4) a'); 49 | 50 | assert.instanceOf(parser.factor(), AST.UnaryOperator); 51 | assert.instanceOf(parser.factor(), AST.UnaryOperator); 52 | assert.instanceOf(parser.factor(), AST.Number); 53 | assert.instanceOf(parser.factor(), AST.Number); 54 | assert.instanceOf(parser.factor(), AST.Variable); 55 | }); 56 | 57 | it('Should properly handle term production', () => { 58 | const parser = new Parser('1 * 2 / 3'); 59 | 60 | assert.instanceOf(parser.term(), AST.BinaryOperator); 61 | }); 62 | 63 | it('Should properly handle expr production', () => { 64 | const parser = new Parser('1 + 2 - 3'); 65 | 66 | assert.instanceOf(parser.expr(), AST.BinaryOperator); 67 | }); 68 | 69 | it('Should properly handle assignmentStatement production', () => { 70 | const parser = new Parser('a := 2'); 71 | const node = parser.assignmentStatement(); 72 | 73 | assert.instanceOf(node, AST.Assign); 74 | assert.instanceOf(node.getVariable(), AST.Variable); 75 | assert.equal(node.getVariable().getName(), 'a'); 76 | assert.instanceOf(node.getToken(), Token); 77 | assert.ok(node.getToken().is(Token.ASSIGN)); 78 | assert.instanceOf(node.getExpression(), AST.Number); 79 | assert.equal(node.getExpression().getValue(), 2); 80 | }); 81 | 82 | it('Should properly handle compoundStatement production', () => { 83 | const parser = new Parser('BEGIN a := 2 END'); 84 | const node = parser.compoundStatement(); 85 | 86 | assert.instanceOf(node, AST.Compound); 87 | assert.equal(node.getChildren().length, 1); 88 | assert.instanceOf(node.getChildren()[0], AST.Assign); 89 | assert.equal(node.getChildren()[0].getVariable().getName(), 'a'); 90 | assert.ok(node.getChildren()[0].getToken().is(Token.ASSIGN)); 91 | assert.equal(node.getChildren()[0].getExpression().getValue(), 2); 92 | }); 93 | 94 | it('Should properly handle statement production', () => { 95 | const parser = new Parser('BEGIN END'); 96 | const node = parser.statement(); 97 | 98 | assert.instanceOf(node, AST.Compound); 99 | assert.equal(node.getChildren().length, 1); 100 | assert.instanceOf(node.getChildren()[0], AST.NoOperation); 101 | }); 102 | 103 | it('Should properly handle statementList production', () => { 104 | const parser = new Parser('a := 2; b := 3;'); 105 | const nodes = parser.statementList(); 106 | 107 | assert.isArray(nodes); 108 | assert.equal(nodes.length, 3); 109 | assert.instanceOf(nodes[0], AST.Assign); 110 | assert.equal(nodes[0].getVariable().getName(), 'a'); 111 | assert.ok(nodes[0].getToken().is(Token.ASSIGN)); 112 | assert.equal(nodes[0].getExpression().getValue(), 2); 113 | assert.instanceOf(nodes[1], AST.Assign); 114 | assert.equal(nodes[1].getVariable().getName(), 'b'); 115 | assert.ok(nodes[1].getToken().is(Token.ASSIGN)); 116 | assert.equal(nodes[1].getExpression().getValue(), 3); 117 | assert.instanceOf(nodes[2], AST.NoOperation); 118 | }); 119 | 120 | it('Should properly handle program production', () => { 121 | const parser = new Parser('PROGRAM program; BEGIN END.'); 122 | const node = parser.program(); 123 | 124 | assert.instanceOf(node, AST.Program); 125 | assert.equal(node.getName(), 'program'); 126 | assert.instanceOf(node.getBlock(), AST.Block); 127 | }); 128 | 129 | it('Should properly parse a simple program', () => { 130 | const program = `PROGRAM program; BEGIN x:= 2; y:= x + 5; END.`; 131 | const parser = new Parser(program); 132 | const ast = parser.parse(); 133 | 134 | assert.instanceOf(parser, Parser); 135 | assert.instanceOf(ast, AST.Program); 136 | }); 137 | 138 | it('Should properly parse a program with all factors', () => { 139 | const program = `PROGRAM program; BEGIN x:= +2; y:= x + -5; z:= (x + y); END.`; 140 | const parser = new Parser(program); 141 | const ast = parser.parse(); 142 | 143 | assert.instanceOf(parser, Parser); 144 | assert.instanceOf(ast, AST.Program); 145 | }); 146 | 147 | it('Should properly parse a program with all terms', () => { 148 | const program = `PROGRAM program; BEGIN x:= +2; y:= x * -5; z:= (x / y); END.`; 149 | const parser = new Parser(program); 150 | const ast = parser.parse(); 151 | 152 | assert.instanceOf(parser, Parser); 153 | assert.instanceOf(ast, AST.Program); 154 | }); 155 | 156 | it('Should properly parse a program with all expressions', () => { 157 | const program = `PROGRAM program; BEGIN x:= +2; y:= x * -5; z:= (x / y) - 5; END.`; 158 | const parser = new Parser(program); 159 | const ast = parser.parse(); 160 | 161 | assert.instanceOf(parser, Parser); 162 | assert.instanceOf(ast, AST.Program); 163 | }); 164 | 165 | it('Should properly parse a program with few compound statements', () => { 166 | const program = `PROGRAM program; BEGIN x:= +2; y:= x * -5; BEGIN z:= (x / y) - 5; END END.`; 167 | const parser = new Parser(program); 168 | const ast = parser.parse(); 169 | 170 | assert.instanceOf(parser, Parser); 171 | assert.instanceOf(ast, AST.Program); 172 | }); 173 | 174 | it('Should properly parse a program with procedures and parameters', () => { 175 | const program = `PROGRAM program; PROCEDURE Foo(a, b: INTEGER; c: REAL); BEGIN END; BEGIN END.`; 176 | const parser = new Parser(program); 177 | const ast = parser.parse(); 178 | 179 | assert.instanceOf(parser, Parser); 180 | assert.instanceOf(ast, AST.Program); 181 | }); 182 | 183 | it('Should properly parse a program with procedures and no parameters', () => { 184 | const program = `PROGRAM program; PROCEDURE Foo; BEGIN END; BEGIN END.`; 185 | const parser = new Parser(program); 186 | const ast = parser.parse(); 187 | 188 | assert.instanceOf(parser, Parser); 189 | assert.instanceOf(ast, AST.Program); 190 | }); 191 | 192 | it('Should properly throw an error if provide parser with unexpected chars', () => { 193 | assert.throws(() => new Parser('~~'), Error, '[Lexer]\nUnexpected character: ~'); 194 | }); 195 | 196 | it('Should properly throw an error if identifier is in statement list', () => { 197 | const program = `PROGRAM program; BEGIN x:= y z END.`; 198 | const parser = new Parser(program); 199 | 200 | assert.throws(() => parser.parse(), Error, '[Parser]\nUnexpected identifier in "statementList" production: Token(IDENTIFIER, z)'); 201 | }); 202 | 203 | it('Should properly throw an error if provide parser with wrong syntax structure', () => { 204 | const parser = new Parser('PROGRAM program; BEGIN x:= 100 + / 5 END.'); 205 | 206 | assert.throws(() => parser.parse(), Error, '[Parser]\nYou provided unexpected token type "IDENTIFIER" while current token is Token(SLASH, /)'); 207 | }); 208 | }); 209 | -------------------------------------------------------------------------------- /test/semantic/SemanticAnalyzer.test.js: -------------------------------------------------------------------------------- 1 | const {assert} = require('chai'); 2 | const SemanticAnalyzer = require('../../src/semantic/SemanticAnalyzer'); 3 | const Parser = require('../../src/parser'); 4 | 5 | describe('Semantic::SemanticAnalyzer', () => { 6 | it('Should properly throw error if variable is not resolved', () => { 7 | const program = ` 8 | PROGRAM main; 9 | VAR x: INTEGER; 10 | BEGIN 11 | x:= y; 12 | END. 13 | `; 14 | const ast = new Parser(program).parse(); 15 | const analyzer = new SemanticAnalyzer(); 16 | 17 | assert.throws(() => analyzer.visit(ast), Error, 'Variable y is not resolved'); 18 | }); 19 | 20 | it('Should properly throw error if duplicate variable declaration', () => { 21 | const program = ` 22 | PROGRAM main; 23 | VAR x: INTEGER; 24 | x: REAL; 25 | BEGIN 26 | x:= y; 27 | END. 28 | `; 29 | const ast = new Parser(program).parse(); 30 | const analyzer = new SemanticAnalyzer(); 31 | 32 | assert.throws(() => analyzer.visit(ast), Error, 'Duplicate declaration of x'); 33 | }); 34 | 35 | it('Should properly traverse an AST and create/remove symbol tables', () => { 36 | const program = ` 37 | PROGRAM main; 38 | VAR x, y: REAL; 39 | 40 | PROCEDURE Alpha(a: INTEGER); 41 | VAR y: INTEGER; 42 | BEGIN 43 | END; 44 | 45 | PROCEDURE Beta(a: INTEGER); 46 | VAR y: REAL; 47 | BEGIN 48 | END; 49 | 50 | BEGIN 51 | END. 52 | `; 53 | 54 | const ast = new Parser(program).parse(); 55 | const analyzer = new SemanticAnalyzer(); 56 | 57 | analyzer.visit(ast); 58 | 59 | assert.isNull(analyzer.scope); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /test/symbols/ProcedureSymbol.test.js: -------------------------------------------------------------------------------- 1 | const {assert} = require('chai'); 2 | const ProcedureSymbol = require('../../src/symbols/ProcedureSymbol'); 3 | 4 | describe('Symbols::ProcedureSymbol', () => { 5 | it('Should properly instantiate', () => { 6 | const symbol = ProcedureSymbol.create('foo'); 7 | 8 | assert.instanceOf(symbol, ProcedureSymbol); 9 | assert.equal(symbol.name, 'foo'); 10 | assert.isArray(symbol.params); 11 | }); 12 | 13 | it('Should properly return params', () => { 14 | const symbol = ProcedureSymbol.create('foo', []); 15 | 16 | assert.instanceOf(symbol, ProcedureSymbol); 17 | assert.isArray(symbol.getParams()); 18 | }); 19 | 20 | it('Should properly convert into string representation', () => { 21 | const symbol = ProcedureSymbol.create('foo', [1, 2]); 22 | 23 | assert.equal(symbol, 'ProcedureSymbol(foo, 1,2)'); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /test/symbols/Symbol.test.js: -------------------------------------------------------------------------------- 1 | const {assert} = require('chai'); 2 | const Symbol = require('../../src/symbols/Symbol'); 3 | 4 | describe('Symbols::Symbol', () => { 5 | it('Should properly instantiate', () => { 6 | const symbol = Symbol.create('INTEGER'); 7 | 8 | assert.isString(symbol.name); 9 | assert.isNull(symbol.type); 10 | }); 11 | 12 | it('Should properly get name of a symbol', () => { 13 | const symbol = Symbol.create('INTEGER'); 14 | 15 | assert.isString(symbol.name); 16 | assert.isNull(symbol.type); 17 | assert.equal(symbol.getName(), 'INTEGER'); 18 | }); 19 | 20 | it('Should properly get type of a symbol', () => { 21 | const symbol = Symbol.create('INTEGER', 'TYPE'); 22 | 23 | assert.isString(symbol.name); 24 | assert.isString(symbol.type); 25 | assert.equal(symbol.getType(), 'TYPE'); 26 | }); 27 | 28 | it('Should properly convert to string representation', () => { 29 | const symbol = Symbol.create('INTEGER', 'TYPE'); 30 | 31 | assert.isString(symbol.name); 32 | assert.isString(symbol.type); 33 | assert.equal(symbol, 'Symbol(INTEGER, TYPE)'); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /test/symbols/SymbolTable.test.js: -------------------------------------------------------------------------------- 1 | const {assert} = require('chai'); 2 | const SymbolTable = require('../../src/symbols/SymbolTable'); 3 | const TypeSymbol = require('../../src/symbols/TypeSymbol'); 4 | const VariableSymbol = require('../../src/symbols/VariableSymbol'); 5 | 6 | describe('Symbols::SymbolTable', () => { 7 | it('Should properly instantiate', () => { 8 | const table = SymbolTable.create('global', 1); 9 | 10 | assert.instanceOf(table, SymbolTable); 11 | assert.instanceOf(table.symbols, Map); 12 | assert.equal(table.symbols.size, 2); 13 | assert.instanceOf(table.symbols.get('INTEGER'), TypeSymbol); 14 | assert.instanceOf(table.symbols.get('REAL'), TypeSymbol); 15 | assert.equal(table.scopeName, 'global'); 16 | assert.equal(table.scopeLevel, 1); 17 | assert.isNull(table.enclosingScope); 18 | }); 19 | 20 | it('Should properly define a symbol', () => { 21 | const table = SymbolTable.create('global', 1); 22 | const typeSymbol = TypeSymbol.create('INTEGER'); 23 | const variableSymbol = VariableSymbol.create('x', typeSymbol); 24 | 25 | assert.instanceOf(table, SymbolTable); 26 | assert.instanceOf(table.symbols, Map); 27 | assert.equal(table.symbols.size, 2); 28 | assert.instanceOf(table.define(typeSymbol), SymbolTable); 29 | assert.equal(table.symbols.size, 2); 30 | assert.instanceOf(table.define(variableSymbol), SymbolTable); 31 | assert.equal(table.symbols.size, 3); 32 | }); 33 | 34 | it('Should properly lookup a symbol', () => { 35 | const table = SymbolTable.create('global', 1); 36 | const typeSymbol = TypeSymbol.create('INTEGER'); 37 | const variableSymbol = VariableSymbol.create('x', typeSymbol); 38 | 39 | assert.instanceOf(table, SymbolTable); 40 | assert.instanceOf(table.symbols, Map); 41 | assert.equal(table.symbols.size, 2); 42 | assert.instanceOf(table.define(typeSymbol), SymbolTable); 43 | assert.equal(table.symbols.size, 2); 44 | assert.instanceOf(table.define(variableSymbol), SymbolTable); 45 | assert.equal(table.symbols.size, 3); 46 | assert.instanceOf(table.lookup('INTEGER'), TypeSymbol); 47 | assert.instanceOf(table.lookup('x'), VariableSymbol); 48 | assert.isUndefined(table.lookup('y')); 49 | }); 50 | 51 | it('Should properly converts into string representation', () => { 52 | const table = SymbolTable.create('global', 1); 53 | 54 | assert.instanceOf(table, SymbolTable); 55 | assert.equal(table, `Scope Name: global\nScope Level: 1\n\nEntries\nINTEGER:TypeSymbol(INTEGER)\nREAL:TypeSymbol(REAL)\n`); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /test/symbols/SymbolTableBuilder.test.js: -------------------------------------------------------------------------------- 1 | const {assert} = require('chai'); 2 | const SymbolTableBuilder = require('../../src/symbols/SymbolTableBuilder'); 3 | const SymbolTable = require('../../src/symbols/SymbolTable'); 4 | const Parser = require('../../src/parser'); 5 | 6 | describe('Symbols::SymbolTableBuilder', () => { 7 | it('Should properly instantiate', () => { 8 | const builder = SymbolTableBuilder.create(); 9 | 10 | assert.instanceOf(builder, SymbolTableBuilder); 11 | assert.instanceOf(builder.scope, SymbolTable); 12 | }); 13 | 14 | it('Should properly build a symbol table for a simple program', () => { 15 | const program = `PROGRAM program; VAR x: INTEGER; y: REAL; BEGIN END.`; 16 | const parser = new Parser(program); 17 | const ast = parser.parse(); 18 | const tableBuilder = SymbolTableBuilder.create(); 19 | 20 | tableBuilder.visit(ast); 21 | 22 | assert.equal(tableBuilder.scope.symbols.size, 4); 23 | assert.equal(tableBuilder.scope.lookup('x').getName(), 'x'); 24 | assert.equal(tableBuilder.scope.lookup('y').getName(), 'y'); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /test/symbols/TypeSymbol.test.js: -------------------------------------------------------------------------------- 1 | const {assert} = require('chai'); 2 | const TypeSymbol = require('../../src/symbols/TypeSymbol'); 3 | 4 | describe('Symbols::TypeSymbol', () => { 5 | it('Should properly instantiate', () => { 6 | const symbol = TypeSymbol.create('INTEGER'); 7 | 8 | assert.instanceOf(symbol, TypeSymbol); 9 | assert.isString(symbol.name); 10 | assert.isNull(symbol.type); 11 | }); 12 | 13 | it('Should properly converts to string representation', () => { 14 | const symbol = TypeSymbol.create('REAL'); 15 | 16 | assert.instanceOf(symbol, TypeSymbol); 17 | assert.equal(symbol, 'TypeSymbol(REAL)'); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /test/symbols/VariableSymbol.test.js: -------------------------------------------------------------------------------- 1 | const {assert} = require('chai'); 2 | const VariableSymbol = require('../../src/symbols/VariableSymbol'); 3 | const TypeSymbol = require('../../src/symbols/TypeSymbol'); 4 | 5 | describe('Symbols::VariableSymbol', () => { 6 | it('Should properly instantiate', () => { 7 | const symbol = VariableSymbol.create('x', TypeSymbol.create('INTEGER')); 8 | 9 | assert.isString(symbol.name); 10 | assert.instanceOf(symbol.type, TypeSymbol); 11 | }); 12 | 13 | it('Should properly convert into string representation', () => { 14 | const symbol = VariableSymbol.create('x', TypeSymbol.create('INTEGER')); 15 | 16 | assert.isString(symbol.name); 17 | assert.instanceOf(symbol.type, TypeSymbol); 18 | assert.equal(symbol, 'VariableSymbol(x, TypeSymbol(INTEGER))'); 19 | }); 20 | }); 21 | --------------------------------------------------------------------------------