├── .gitignore ├── README.md ├── benchmark.txt ├── functions ├── arr.js ├── date.js ├── fs.js ├── http.js ├── mth.js ├── print.js ├── string.js └── timer.js ├── index.ts ├── myprogram.ay ├── myprogram.js ├── package-lock.json ├── package.json ├── parser ├── astcompiler.ts ├── asts.ts ├── parser.ts └── tokens.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | node_modules 3 | dist/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AY Language Compiler 2 | 3 | ## Introduction 4 | 5 | **AY** is a simple, custom programming language designed for experimentation and learning. This compiler translates AY programs into JavaScript, allowing you to run your AY code in any JavaScript environment. 6 | 7 | ## Installation 8 | 9 | ### Option 1: Install from npm (Recommended) 10 | 11 | Install the AY compiler globally using npm: 12 | 13 | ```bash 14 | npm install -g ayscript 15 | ``` 16 | 17 | ### Option 2: Install from source 18 | 19 | Clone the repository: 20 | 21 | ```bash 22 | git clone https://github.com/MikeyA-yo/ay-ts.git 23 | ``` 24 | 25 | Navigate to the project directory: 26 | 27 | ```bash 28 | cd ay-ts 29 | ``` 30 | 31 | Install dependencies: 32 | 33 | ```bash 34 | npm install 35 | ``` 36 | 37 | Build the project: 38 | 39 | ```bash 40 | npx tsc 41 | ``` 42 | 43 | ## Usage 44 | 45 | To compile and run an AY program: 46 | 47 | ### Using the global installation 48 | 49 | 1. **Write your AY code** in a file with the `.ay` extension (e.g., `myprogram.ay`). 50 | 51 | 2. **Compile the program:** 52 | 53 | ```bash 54 | ayc myprogram.ay 55 | ``` 56 | 57 | 3. **The compiler will generate** a JavaScript file (e.g., `myprogram.js`). 58 | 59 | 4. **Run the generated JavaScript file** using Node.js: 60 | 61 | ```bash 62 | node myprogram.js 63 | ``` 64 | 65 | ### Using the source installation 66 | 67 | 1. **Write your AY code** in a file with the `.ay` extension (e.g., `myprogram.ay`). 68 | 69 | 2. **Compile the program:** 70 | 71 | ```bash 72 | node dist/index.js myprogram.ay 73 | ``` 74 | 75 | 3. **The compiler will generate** a JavaScript file (e.g., `myprogram.js`). 76 | 77 | 4. **Run the generated JavaScript file** using Node.js: 78 | 79 | ```bash 80 | node myprogram.js 81 | ``` 82 | 83 | ## Language Features 84 | 85 | ### Variable Declaration 86 | 87 | Variables are declared using the `l` keyword and are block-scoped: 88 | 89 | ```ay 90 | l a = "hello"; 91 | l b = 42; 92 | ``` 93 | 94 | ### Functions 95 | 96 | Functions are declared using the `f` keyword: 97 | 98 | ```ay 99 | f add(a, b) { 100 | l c = a + b; 101 | return c; 102 | } 103 | ``` 104 | 105 | ### Aliases with `def` Keyword 106 | 107 | The `def` keyword allows you to create aliases for language constructs, making your code more readable: 108 | 109 | ```ay 110 | // Create aliases for common keywords 111 | def var -> l 112 | def fn -> f 113 | def brk -> break 114 | def cnt -> continue 115 | 116 | // Now use your custom aliases 117 | var userName = "Alice" 118 | fn greet(name) { 119 | return "Hello, " + name + "!" 120 | } 121 | 122 | var message = greet(userName) 123 | print(message) 124 | 125 | // Use in control flow 126 | var counter = 0 127 | while (counter < 10) { 128 | if (counter == 5) { 129 | brk // Using the alias for break 130 | } 131 | counter++ 132 | } 133 | ``` 134 | 135 | ### Control Flow 136 | 137 | #### If-Else 138 | 139 | ```ay 140 | if (a == b) { 141 | return true; 142 | } else if (c == b) { 143 | return "true"; 144 | } else { 145 | return false; 146 | } 147 | ``` 148 | 149 | #### Loops 150 | 151 | **While Loop:** 152 | 153 | ```ay 154 | l i = 0; 155 | while (i < 5) { 156 | print(i); 157 | i++; 158 | } 159 | ``` 160 | 161 | **For Loop:** 162 | 163 | ```ay 164 | for (l i = 0; i < 8; i++) { 165 | print(i); 166 | } 167 | ``` 168 | 169 | ### Comments 170 | 171 | **Single-line comments:** 172 | 173 | ```ay 174 | // This is a single-line comment 175 | ``` 176 | 177 | **Multi-line comments:** 178 | 179 | ```ay 180 | /* 181 | This is a multi-line comment. 182 | */ 183 | ``` 184 | 185 | ### Standard Library 186 | 187 | The AY language includes a comprehensive standard library with the following functions: 188 | 189 | #### Core Functions 190 | 191 | - **`print(...values)`** - Prints values to the console 192 | - **`input(prompt?)`** - Gets user input from terminal (synchronous, blocks execution) 193 | - **`coolPrint(msg)`** - Prints message with "[COOL PRINT]" prefix 194 | - **`fancyLog(msg)`** - Prints message with "✨ FANCY LOG:" prefix 195 | - **`stylishWarn(msg)`** - Prints warning with "⚠️ STYLISH WARNING:" prefix 196 | - **`errorPop(msg)`** - Prints error with "❌ ERROR POP:" prefix 197 | - **`errorlog(...msg)`** - Prints error messages to console 198 | - **`rand(min?, max?)`** - Returns random number (0-1 if no params, or between min-max) 199 | - **`randInt(min?, max?)`** - Returns random integer between min and max 200 | - **`round(num, precision?)`** - Rounds number to specified decimal places (default 0) 201 | 202 | #### Math Functions 203 | 204 | ```ay 205 | abs(x), floor(x), ceil(x), round(x), max(a, b), min(a, b) 206 | sqrt(x), pow(x, y), log(x), exp(x) 207 | sin(x), cos(x), tan(x), asin(x), acos(x), atan(x) 208 | toRadians(x), toDegrees(x) 209 | ``` 210 | 211 | #### String Functions 212 | 213 | ```ay 214 | len(s), upper(s), lower(s), trim(s) 215 | split(s, delimiter), join(arr, delimiter) 216 | reverse(s), replace(s, old, new), includes(s, substr) 217 | ``` 218 | 219 | #### Array Functions 220 | 221 | ```ay 222 | push(arr, ...values), pop(arr), shift(arr), unshift(arr, ...values) 223 | sort(arr), reverse(arr), slice(arr, start, end) 224 | includes(arr, value), indexOf(arr, value) 225 | ``` 226 | 227 | #### File System Functions 228 | 229 | ```ay 230 | readFile(path), writeFile(path, content), appendFile(path, content) 231 | ``` 232 | 233 | #### HTTP Functions 234 | 235 | ```ay 236 | httpGet(url), httpPost(url, data) 237 | awaitPromise(promise, onSuccess, onError) 238 | ``` 239 | 240 | #### Date/Time Functions 241 | 242 | ```ay 243 | now(), sleep(ms) 244 | ``` 245 | 246 | ### Example Using Built-in Functions 247 | 248 | ```ay 249 | // Math operations 250 | l numbers = [5, 2, 8, 1, 9] 251 | l maxNum = max(5, 10) // 10 252 | l rounded = round(3.7, 1) // 3.7 (rounded to 1 decimal place) 253 | 254 | // String operations 255 | l text = "Hello World" 256 | l upperText = upper(text) // "HELLO WORLD" 257 | l textLength = len(text) // 11 258 | l words = split(text, " ") // ["Hello", "World"] 259 | l reversedText = reverse(text) // "dlroW olleH" 260 | 261 | // User input (synchronous) 262 | l userName = input("Enter your name: ") 263 | print("Hello, " + userName + "!") 264 | 265 | l age = input("Enter your age: ") 266 | l ageNum = parseInt(age) 267 | if (ageNum >= 18) { 268 | print("You are an adult!") 269 | } else { 270 | print("You are a minor!") 271 | } 272 | 273 | // Array operations 274 | l fruits = ["apple", "banana"] 275 | l moreFruits = push(fruits, "orange", "grape") // ["apple", "banana", "orange", "grape"] 276 | l sortedFruits = sort(moreFruits) // ["apple", "banana", "grape", "orange"] 277 | l firstTwo = slice(sortedFruits, 0, 2) // ["apple", "banana"] 278 | l hasApple = includes(sortedFruits, "apple") // true 279 | 280 | // Using def with built-ins 281 | def log -> print 282 | def length -> len 283 | def ask -> input 284 | 285 | log("Array length: " + length(sortedFruits)) 286 | l hobby = ask("What's your hobby? ") 287 | log("Cool! You enjoy " + hobby) 288 | 289 | // Math with new functions 290 | l angle = 45 291 | l radians = toRadians(angle) 292 | l sineValue = sin(radians) 293 | l randomBetween = randInt(1, 100) // Random integer between 1-100 294 | log("sin(45°) = " + sineValue) 295 | log("Random number: " + randomBetween) 296 | ``` 297 | 298 | ### Example Program 299 | 300 | ```ay 301 | // Define custom aliases for better readability 302 | def var -> l 303 | def fn -> f 304 | 305 | // Variables and basic operations 306 | var message = "Welcome to AY Language!" 307 | var numbers = [3, 1, 4, 1, 5, 9] 308 | 309 | // String operations 310 | var upperMessage = upper(message) 311 | print(upperMessage) 312 | 313 | // Array operations 314 | var sortedNumbers = sort(numbers) 315 | var arrayLength = len(sortedNumbers) 316 | print("Sorted array: " + sortedNumbers) 317 | print("Length: " + arrayLength) 318 | 319 | // Math operations 320 | var randomValue = round(rand() * 100) 321 | var maxValue = max(randomValue, 50) 322 | print("Random value: " + randomValue) 323 | print("Max of random and 50: " + maxValue) 324 | 325 | // Functions with multiple parameters 326 | fn calculate(a, b, c, d) { 327 | var sum = a + b + c + d 328 | var average = sum / 4 329 | return average 330 | } 331 | 332 | var result = calculate(10, 20, 30, 40) // 25 333 | print("Average: " + result) 334 | 335 | // Control flow 336 | var counter = 0 337 | while (counter < 5) { 338 | if (counter == 3) { 339 | print("Halfway there!") 340 | } 341 | print("Counter: " + counter) 342 | counter++ 343 | } 344 | 345 | // HTTP operations (async) 346 | var promise = httpGet("https://api.github.com/users/octocat") 347 | awaitPromise(promise, fn(data) { 348 | print("GitHub user data received!") 349 | }, fn(error) { 350 | print("Error: " + error) 351 | }) 352 | ``` 353 | 354 | ## Extending the Language 355 | 356 | To add new features to the AY language: 357 | 358 | 1. **Modify the parser** in `parser/` to recognize new syntax. 359 | 2. **Update the AST compiler** in `parser/astcompiler.ts` to handle new AST nodes. 360 | 3. **(Optional) Add new functions** to the standard library in `functions/`. 361 | 362 | ## License 363 | 364 | This project is licensed under the **MIT License**. 365 | -------------------------------------------------------------------------------- /benchmark.txt: -------------------------------------------------------------------------------- 1 | loop of 100000 time 2 | node - 54.952 3 | py - 26.721 4 | bun - 25.699 5 | ay - 31.488 6 | go(with build) - 26.272s 7 | ay-bun - 25.459 8 | C - 22.572 9 | loop of 1000000 time 10 | py - 4m 28.107 11 | bun - 4m 10.512 12 | node - 9m 8. 123 13 | go - 4m 27.954 14 | ay - 5m 8.446 15 | ay-bun - 4m 8.162 16 | C - 3m 57.803s -------------------------------------------------------------------------------- /functions/arr.js: -------------------------------------------------------------------------------- 1 | function sort(arr, compareFn) { 2 | if (!Array.isArray(arr)) { 3 | console.error('Input must be an array'); 4 | process.exit(1) 5 | } 6 | if (!compareFn) { 7 | return arr.sort(); 8 | } else { 9 | return arr.sort(compareFn); 10 | } 11 | } 12 | 13 | function reverse(arr) { 14 | if (!Array.isArray(arr)) { 15 | console.error('Input must be an array'); 16 | process.exit(1) 17 | } 18 | return arr.reverse(); 19 | } 20 | 21 | function filter(arr, callback) { 22 | if (!Array.isArray(arr)) { 23 | console.error('Input must be an array'); 24 | process.exit(1) 25 | } 26 | return arr.filter(callback); 27 | } 28 | 29 | function map(arr, callback) { 30 | if (!Array.isArray(arr)) { 31 | console.error('Input must be an array'); 32 | process.exit(1) 33 | } 34 | return arr.map(callback); 35 | } 36 | 37 | function slice(arr, start, end) { 38 | if (!Array.isArray(arr)) { 39 | console.error('Input must be an array'); 40 | process.exit(1) 41 | } 42 | return arr.slice(start, end); 43 | } 44 | 45 | function splice(arr, start, deleteCount, ...items) { 46 | if (!Array.isArray(arr)) { 47 | console.error('Input must be an array'); 48 | process.exit(1) 49 | } 50 | arr.splice(start, deleteCount,...items); 51 | return arr; 52 | } 53 | 54 | function push(arr,...items) { 55 | if (!Array.isArray(arr)) { 56 | console.error('Input must be an array'); 57 | process.exit(1) 58 | } 59 | arr.push(...items); 60 | return arr; 61 | } 62 | 63 | function pop(arr) { 64 | if (!Array.isArray(arr)) { 65 | console.error('Input must be an array but got,', typeof arr, 'instead for this function'); 66 | process.exit(1) 67 | } 68 | arr.pop(); 69 | return arr; 70 | } 71 | 72 | function len(arr) { 73 | if (!Array.isArray(arr) && typeof arr !== "string") { 74 | console.error('Input must be an array or string but got,', typeof arr, 'instead for this function'); 75 | process.exit(1) 76 | } 77 | return arr.length; 78 | } 79 | 80 | function newArr(arr, size, fillValue = null){ 81 | if (!Array.isArray(arr)) { 82 | console.error('Input must be an array'); 83 | process.exit(1) 84 | } 85 | return Array.from({ length: size }, (_, i) => arr[i] || fillValue); 86 | } 87 | function includes(arr, value) { 88 | if (!Array.isArray(arr) && typeof arr !== "string") { 89 | console.error('Input must be an array or string but got,', typeof arr, 'instead for this function'); 90 | process.exit(1) 91 | } 92 | return arr.includes(value); 93 | } -------------------------------------------------------------------------------- /functions/date.js: -------------------------------------------------------------------------------- 1 | // Basic date utility functions 2 | const dateToISO = (date) => { 3 | const d = date instanceof Date ? date : new Date(date); 4 | return d.toISOString(); 5 | }; 6 | 7 | const dateToLocal = (date) => { 8 | const d = date instanceof Date ? date : new Date(date); 9 | return d.toLocaleString(); 10 | }; 11 | 12 | const dateToShort = (date) => { 13 | const d = date instanceof Date ? date : new Date(date); 14 | return `${d.getMonth() + 1}/${d.getDate()}/${d.getFullYear()}`; 15 | }; 16 | 17 | const dateToLong = (date) => { 18 | const d = date instanceof Date ? date : new Date(date); 19 | return d.toLocaleDateString('en-US', { 20 | weekday: 'long', 21 | year: 'numeric', 22 | month: 'long', 23 | day: 'numeric' 24 | }); 25 | }; 26 | 27 | const dateDiffInDays = (date1, date2) => { 28 | const d1 = new Date(date1); 29 | const d2 = new Date(date2); 30 | const diffTime = Math.abs(d2 - d1); 31 | return Math.ceil(diffTime / (1000 * 60 * 60 * 24)); 32 | }; 33 | 34 | const dateDiffInHours = (date1, date2) => { 35 | const d1 = new Date(date1); 36 | const d2 = new Date(date2); 37 | const diffTime = Math.abs(d2 - d1); 38 | return Math.ceil(diffTime / (1000 * 60 * 60)); 39 | }; 40 | 41 | const dateDiffInMinutes = (date1, date2) => { 42 | const d1 = new Date(date1); 43 | const d2 = new Date(date2); 44 | const diffTime = Math.abs(d2 - d1); 45 | return Math.ceil(diffTime / (1000 * 60)); 46 | }; 47 | 48 | const dateDiffInSeconds = (date1, date2) => { 49 | const d1 = new Date(date1); 50 | const d2 = new Date(date2); 51 | const diffTime = Math.abs(d2 - d1); 52 | return Math.ceil(diffTime / 1000); 53 | }; 54 | 55 | // Advanced date manipulation 56 | const dateAdd = (date, value, unit) => { 57 | const d = new Date(date); 58 | switch(unit.toLowerCase()) { 59 | case 'years': d.setFullYear(d.getFullYear() + value); break; 60 | case 'months': d.setMonth(d.getMonth() + value); break; 61 | case 'days': d.setDate(d.getDate() + value); break; 62 | case 'hours': d.setHours(d.getHours() + value); break; 63 | case 'minutes': d.setMinutes(d.getMinutes() + value); break; 64 | case 'seconds': d.setSeconds(d.getSeconds() + value); break; 65 | } 66 | return d; 67 | }; 68 | 69 | const dateSubtract = (date, value, unit) => { 70 | return dateAdd(date, -value, unit); 71 | }; 72 | 73 | const dateStartOf = (date, unit) => { 74 | const d = new Date(date); 75 | switch(unit.toLowerCase()) { 76 | case 'year': d.setMonth(0, 1); d.setHours(0, 0, 0, 0); break; 77 | case 'month': d.setDate(1); d.setHours(0, 0, 0, 0); break; 78 | case 'day': d.setHours(0, 0, 0, 0); break; 79 | case 'hour': d.setMinutes(0, 0, 0); break; 80 | } 81 | return d; 82 | }; 83 | 84 | const dateEndOf = (date, unit) => { 85 | const d = new Date(date); 86 | switch(unit.toLowerCase()) { 87 | case 'year': d.setMonth(11, 31); d.setHours(23, 59, 59, 999); break; 88 | case 'month': d.setMonth(d.getMonth() + 1, 0); d.setHours(23, 59, 59, 999); break; 89 | case 'day': d.setHours(23, 59, 59, 999); break; 90 | case 'hour': d.setMinutes(59, 59, 999); break; 91 | } 92 | return d; 93 | }; 94 | 95 | // Helper function to check if date is valid 96 | const isValidDateFormat = (date) => { 97 | const d = new Date(date); 98 | return d instanceof Date && !isNaN(d); 99 | }; 100 | 101 | function now(){ 102 | return new Date(); 103 | } 104 | function timestamp(){ 105 | return Date.now(); 106 | } -------------------------------------------------------------------------------- /functions/fs.js: -------------------------------------------------------------------------------- 1 | const { readFileSync, writeFileSync } = require("node:fs"); 2 | 3 | function read(path, options = "utf-8"){ 4 | return readFileSync(path, options) 5 | } 6 | 7 | function write(file, data){ 8 | return writeFileSync(file, data); 9 | } -------------------------------------------------------------------------------- /functions/http.js: -------------------------------------------------------------------------------- 1 | // Functional HTTP utilities for AY language 2 | // All functions are pure and functional - no side effects, immutable data 3 | 4 | // Simple GET Request 5 | function httpGet(url, parseType = "json") { 6 | return fetch(url) 7 | .then((response) => { 8 | if (parseType === "json") { 9 | return response.json(); 10 | } else if (parseType === "text") { 11 | return response.text(); 12 | } else if (parseType === "blob") { 13 | return response.blob(); 14 | } else if (parseType === "arrayBuffer") { 15 | return response.arrayBuffer(); 16 | } else if (parseType === "formData") { 17 | return response.formData(); 18 | } else { 19 | // Default to json if unknown type 20 | return response.json(); 21 | } 22 | }) 23 | .catch((error) => ({ error: error.message, success: false })); 24 | } 25 | 26 | // Simple POST Request 27 | function httpPost(url, data) { 28 | return fetch(url, { 29 | method: "POST", 30 | headers: { "Content-Type": "application/json" }, 31 | body: JSON.stringify(data), 32 | }) 33 | .then((response) => response.json()) 34 | .catch((error) => ({ error: error.message, success: false })); 35 | } 36 | 37 | // Simple PUT Request 38 | function httpPut(url, data) { 39 | return fetch(url, { 40 | method: "PUT", 41 | headers: { "Content-Type": "application/json" }, 42 | body: JSON.stringify(data), 43 | }) 44 | .then((response) => response.json()) 45 | .catch((error) => ({ error: error.message, success: false })); 46 | } 47 | 48 | // Simple DELETE Request 49 | function httpDelete(url) { 50 | return fetch(url, { 51 | method: "DELETE", 52 | }) 53 | .then((response) => response.json()) 54 | .catch((error) => ({ error: error.message, success: false })); 55 | } 56 | 57 | // Create HTTP Server 58 | function createHttpServer(port) { 59 | const http = require("http"); 60 | const url = require("url"); 61 | 62 | const server = http.createServer((req, res) => { 63 | res.setHeader("Access-Control-Allow-Origin", "*"); 64 | res.setHeader( 65 | "Access-Control-Allow-Methods", 66 | "GET, POST, PUT, DELETE, OPTIONS" 67 | ); 68 | res.setHeader( 69 | "Access-Control-Allow-Headers", 70 | "Content-Type, Authorization" 71 | ); 72 | 73 | if (req.method === "OPTIONS") { 74 | res.writeHead(200); 75 | res.end(); 76 | return; 77 | } 78 | 79 | const parsedUrl = url.parse(req.url, true); 80 | const path = parsedUrl.pathname; 81 | const method = req.method; 82 | 83 | let body = ""; 84 | req.on("data", (chunk) => { 85 | body += chunk.toString(); 86 | }); 87 | 88 | req.on("end", () => { 89 | const response = { 90 | message: "Hello from AY HTTP Server!", 91 | method: method, 92 | path: path, 93 | query: parsedUrl.query, 94 | timestamp: new Date().toISOString(), 95 | }; 96 | 97 | res.writeHead(200, { "Content-Type": "application/json" }); 98 | res.end(JSON.stringify(response)); 99 | }); 100 | }); 101 | 102 | server.listen(port, () => { 103 | console.log(`AY HTTP Server running on port ${port}`); 104 | }); 105 | 106 | return server; 107 | } 108 | 109 | // Start Server 110 | function startHttpServer(port) { 111 | return createHttpServer(port); 112 | } 113 | 114 | // Stop Server 115 | function stopHttpServer(server) { 116 | if (server && server.close) { 117 | server.close(); 118 | return true; 119 | } 120 | return false; 121 | } 122 | 123 | // JSON Response Helper 124 | function createJsonResponse(data, status) { 125 | return { 126 | status: status || 200, 127 | data: data, 128 | timestamp: new Date().toISOString(), 129 | }; 130 | } 131 | 132 | // Error Response Helper 133 | function createErrorResponse(message, status) { 134 | return { 135 | status: status || 500, 136 | error: message, 137 | timestamp: new Date().toISOString(), 138 | }; 139 | } 140 | 141 | // Success Response Helper 142 | function createSuccessResponse(data, message) { 143 | return { 144 | status: 200, 145 | success: true, 146 | message: message || "Success", 147 | data: data, 148 | timestamp: new Date().toISOString(), 149 | }; 150 | } 151 | 152 | // URL Builder 153 | function buildHttpUrl(base, path) { 154 | return base + path; 155 | } 156 | 157 | // Query String Builder 158 | function buildQueryString(params) { 159 | const query = new URLSearchParams(); 160 | for (const key in params) { 161 | query.append(key, params[key]); 162 | } 163 | return query.toString(); 164 | } 165 | 166 | // Parse JSON 167 | function parseJson(jsonString) { 168 | try { 169 | return JSON.parse(jsonString); 170 | } catch (error) { 171 | return { error: "Invalid JSON", success: false }; 172 | } 173 | } 174 | 175 | // Stringify JSON 176 | function stringifyJson(obj) { 177 | try { 178 | return JSON.stringify(obj); 179 | } catch (error) { 180 | return "{}"; 181 | } 182 | } 183 | 184 | // HTTP Status Helper 185 | function getHttpStatusMessage(status) { 186 | const statusMessages = { 187 | 200: "OK", 188 | 201: "Created", 189 | 400: "Bad Request", 190 | 401: "Unauthorized", 191 | 403: "Forbidden", 192 | 404: "Not Found", 193 | 500: "Internal Server Error", 194 | }; 195 | return statusMessages[status] || "Unknown Status"; 196 | } 197 | 198 | 199 | 200 | // HTTP Logger 201 | function logHttpRequest(method, url, data) { 202 | const timestamp = new Date().toISOString(); 203 | console.log(`[${timestamp}] ${method} ${url}`); 204 | if (data) { 205 | console.log("Data:", JSON.stringify(data, null, 2)); 206 | } 207 | } 208 | 209 | // HTTP Logger for Response 210 | function logHttpResponse(response) { 211 | const timestamp = new Date().toISOString(); 212 | console.log(`[${timestamp}] Response:`, JSON.stringify(response, null, 2)); 213 | } 214 | 215 | // Promise Resolution Utilities - Functional approach 216 | 217 | // Simple promise resolver - waits for promise and returns result 218 | function awaitPromise(promise, onSuccess, onError) { 219 | return promise.then(onSuccess).catch(onError); 220 | } 221 | 222 | // Promise resolver with timeout 223 | function awaitPromiseWithTimeout(promise, onSuccess, onError, timeout) { 224 | return Promise.race([ 225 | promise, 226 | new Promise((_, reject) => 227 | setTimeout(() => reject(new Error("Promise timeout")), timeout || 5000) 228 | ), 229 | ]) 230 | .then(onSuccess) 231 | .catch(onError); 232 | } 233 | 234 | 235 | 236 | // Promise chain helper - run multiple promises 237 | function awaitAll(promises, onSuccess, onError) { 238 | return Promise.all(promises).then(onSuccess).catch(onError); 239 | } 240 | 241 | 242 | 243 | 244 | // Simple promise logger 245 | function logPromise(promise, label, varName) { 246 | console.log(`Starting promise: ${label || "Unnamed"}`); 247 | return promise 248 | .then((result) => { 249 | console.log(`Promise resolved: ${label || "Unnamed"}`, result); 250 | varName = result; 251 | return result; 252 | }) 253 | .catch((error) => { 254 | console.error(`Promise rejected: ${label || "Unnamed"}`, error.message); 255 | return { error: error.message, success: false }; 256 | }); 257 | } -------------------------------------------------------------------------------- /functions/mth.js: -------------------------------------------------------------------------------- 1 | function rand(min = 0, max = 0) { 2 | return Math.random() * (max - min + 1) + min 3 | } 4 | 5 | function randInt(min = 0, max = 0) { 6 | return Math.floor(Math.random() * (max - min + 1) + min) 7 | } 8 | 9 | function round(num, precision = 0) { 10 | return Math.round(num * Math.pow(10, precision)) / Math.pow(10, precision); 11 | } 12 | 13 | // Basic trigonometric functions 14 | function sin(x) { 15 | return Math.sin(x); 16 | } 17 | 18 | function cos(x) { 19 | return Math.cos(x); 20 | } 21 | 22 | function tan(x) { 23 | return Math.tan(x); 24 | } 25 | 26 | // Inverse trigonometric functions 27 | function asin(x) { 28 | return Math.asin(x); 29 | } 30 | 31 | function acos(x) { 32 | return Math.acos(x); 33 | } 34 | 35 | function atan(x) { 36 | return Math.atan(x); 37 | } 38 | 39 | function atan2(y, x) { 40 | return Math.atan2(y, x); 41 | } 42 | 43 | // Hyperbolic functions 44 | function sinh(x) { 45 | return Math.sinh(x); 46 | } 47 | 48 | function cosh(x) { 49 | return Math.cosh(x); 50 | } 51 | 52 | function tanh(x) { 53 | return Math.tanh(x); 54 | } 55 | 56 | // Inverse hyperbolic functions 57 | function asinh(x) { 58 | return Math.asinh(x); 59 | } 60 | 61 | function acosh(x) { 62 | return Math.acosh(x); 63 | } 64 | 65 | function atanh(x) { 66 | return Math.atanh(x); 67 | } 68 | 69 | // Trigonometric identities and utility functions 70 | function sec(x) { 71 | return 1 / Math.cos(x); 72 | } 73 | 74 | function csc(x) { 75 | return 1 / Math.sin(x); 76 | } 77 | 78 | function cot(x) { 79 | return 1 / Math.tan(x); 80 | } 81 | 82 | // Convert between degrees and radians 83 | function toRadians(degrees) { 84 | return degrees * (Math.PI / 180); 85 | } 86 | 87 | function toDegrees(radians) { 88 | return radians * (180 / Math.PI); 89 | } 90 | 91 | // Degree-based trigonometric functions 92 | function sind(degrees) { 93 | return Math.sin(toRadians(degrees)); 94 | } 95 | 96 | function cosd(degrees) { 97 | return Math.cos(toRadians(degrees)); 98 | } 99 | 100 | function tand(degrees) { 101 | return Math.tan(toRadians(degrees)); 102 | } 103 | 104 | // Additional math constants and functions 105 | function pi() { 106 | return Math.PI; 107 | } 108 | 109 | function e() { 110 | return Math.E; 111 | } 112 | 113 | function abs(x) { 114 | return Math.abs(x); 115 | } 116 | 117 | function sqrt(x) { 118 | return Math.sqrt(x); 119 | } 120 | 121 | function pow(base, exponent) { 122 | return Math.pow(base, exponent); 123 | } 124 | 125 | function exp(x) { 126 | return Math.exp(x); 127 | } 128 | 129 | function log(x) { 130 | return Math.log(x); 131 | } 132 | 133 | function log10(x) { 134 | return Math.log10(x); 135 | } 136 | 137 | function log2(x) { 138 | return Math.log2(x); 139 | } 140 | 141 | function floor(x) { 142 | return Math.floor(x); 143 | } 144 | 145 | function ceil(x) { 146 | return Math.ceil(x); 147 | } 148 | 149 | function max(...numbers) { 150 | return Math.max(...numbers); 151 | } 152 | 153 | function min(...numbers) { 154 | return Math.min(...numbers); 155 | } 156 | -------------------------------------------------------------------------------- /functions/print.js: -------------------------------------------------------------------------------- 1 | // Print utility functions for AY language 2 | function coolPrint(msg) { 3 | console.log("[COOL PRINT]", msg); 4 | } 5 | 6 | function fancyLog(msg) { 7 | console.log("✨ FANCY LOG:", msg); 8 | } 9 | 10 | function stylishWarn(msg) { 11 | console.warn("⚠️ STYLISH WARNING:", msg); 12 | } 13 | 14 | function errorPop(msg) { 15 | console.error("❌ ERROR POP:", msg); 16 | } 17 | 18 | function print(...msg) { 19 | console.log(...msg); 20 | } 21 | 22 | function errorlog(...msg) { 23 | console.error(...msg); 24 | } 25 | 26 | 27 | // Synchronous input function using process.stdin (blocks execution) 28 | const fs = require("fs"); 29 | 30 | function input(prompt = "") { 31 | if (prompt) process.stdout.write(prompt); 32 | 33 | // Read from stdin until newline 34 | const buffer = Buffer.alloc(1024); 35 | const bytesRead = fs.readSync(process.stdin.fd, buffer, 0, buffer.length, null); 36 | 37 | return buffer.toString("utf8", 0, bytesRead).trim(); 38 | } 39 | 40 | function writestdout(...args){ 41 | process.stdout.write(args.join(' ')); 42 | } -------------------------------------------------------------------------------- /functions/string.js: -------------------------------------------------------------------------------- 1 | // String utility functions for AY language 2 | function split(str, delimiter) { 3 | return str.split(delimiter); 4 | } 5 | 6 | function reverse(str) { 7 | return str.split("").reverse().join(""); 8 | } 9 | 10 | function join(arr, delimiter) { 11 | return arr.join(delimiter); 12 | } 13 | 14 | function upper(str) { 15 | return str.toUpperCase(); 16 | } 17 | 18 | function lower(str) { 19 | return str.toLowerCase(); 20 | } -------------------------------------------------------------------------------- /functions/timer.js: -------------------------------------------------------------------------------- 1 | // Timer utility functions for AY language 2 | function Timeout(fn, delay) { 3 | return setTimeout(fn, delay); 4 | } 5 | 6 | function Interval(fn, interval) { 7 | return setInterval(fn, interval); 8 | } 9 | 10 | function stopTimeout(timeoutId) { 11 | clearTimeout(timeoutId); 12 | } 13 | 14 | function stopInterval(intervalId) { 15 | clearInterval(intervalId); 16 | } -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { readFileSync, writeFileSync } from "node:fs"; 3 | import { join } from "node:path"; 4 | import { Parser } from "./parser/parser"; 5 | import compileAST from "./parser/astcompiler"; 6 | import packageJson from "./package.json" 7 | // Get the directory where this module is located 8 | // When compiled to CommonJS, __dirname will be available 9 | declare const __dirname: string; 10 | 11 | const fileName = process.argv[2]; 12 | const VERSION = packageJson.version; 13 | const AY_FancyName = ` 14 | █████╗ ██╗ ██╗ 15 | ██╔══██╗╚██╗ ██╔╝ 16 | ███████║ ╚████╔╝ 17 | ██╔══██║ ╚██╔╝ 18 | ██║ ██║ ██║ 19 | ╚═╝ ╚═╝ ╚═╝ 20 | ` 21 | const welcome = `${AY_FancyName} 22 | AY Programming Language Compiler v${VERSION} 23 | 24 | A modern, expressive programming language that compiles to JavaScript. 25 | Features: Variables (l), Functions (f), Comments, Control Flow, Async Operations, and more! 26 | 27 | Usage: ayc 28 | Example: ayc myprogram.ay 29 | 30 | Visit: https://github.com/MikeyA-yo/ay-ts 31 | `; 32 | if (!fileName) { 33 | console.error(welcome); 34 | console.error("⚠️ No filename provided"); 35 | process.exit(1); 36 | } 37 | 38 | const filePath = join(process.cwd(), fileName); 39 | const fileText = readFileSync(filePath, "utf-8"); 40 | const fileNameParts = fileName.split("."); 41 | if (fileNameParts[fileNameParts.length - 1] !== "ay") { 42 | console.error(welcome); 43 | console.error("⚠️ Invalid file extension. Please use .ay files only."); 44 | process.exit(1); 45 | } 46 | const arrF = readFileSync(join(__dirname, "..", "functions", "arr.js"), "utf-8"); 47 | const mathF = readFileSync(join(__dirname, "..", "functions", "mth.js"), "utf-8"); 48 | const stringF = readFileSync(join(__dirname, "..", "functions", "string.js"), "utf-8"); 49 | const printF = readFileSync(join(__dirname, "..", "functions", "print.js"), "utf-8"); 50 | const fsF = readFileSync(join(__dirname, "..", "functions", "fs.js"), "utf-8"); 51 | const dateF = readFileSync(join(__dirname, "..", "functions", "date.js"), "utf-8"); 52 | const timeF = readFileSync(join(__dirname, "..", "functions", "timer.js"), "utf-8"); 53 | const httpF = readFileSync(join(__dirname, "..", "functions", "http.js"), "utf-8"); 54 | // const mathFancy = ` 55 | // ██╗ ██╗███████╗██████╗ ██╗ ██╗███████╗██████╗ 56 | // ╚██╗ ██╔╝██╔════╝██╔══██╗╚██╗ ██╔╝██╔════╝██╔══██╗ 57 | // ╚████╔╝ █████╗ ██████╔╝ ╚████╔╝ █████╗ ██████╔╝ 58 | // ╚██╔╝ ██╔══╝ ██╔══██╗ ╚██╔╝ ██╔══╝ ██╔══██╗ 59 | // ██║ ███████╗██║ ██║ ██║ ███████╗██║ ██║ 60 | // ╚═╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝ 61 | // ` 62 | const parser = new Parser(fileText); 63 | parser.start(); 64 | if (parser.errors.length > 0) { 65 | 66 | console.error(`${AY_FancyName} Error encountered\nError compiling ${fileName}\n`); 67 | console.error("Errors:"); 68 | parser.errors.forEach((error) => { 69 | console.error(error); 70 | }); 71 | process.exit(1); 72 | } 73 | 74 | const ast = parser.nodes; 75 | const compiled = compileAST(ast); 76 | const output = ` 77 | ${arrF} 78 | ${mathF} 79 | ${stringF} 80 | ${printF} 81 | ${fsF} 82 | ${dateF} 83 | ${timeF} 84 | ${compiled} 85 | ${httpF} 86 | `; 87 | const baseName = fileNameParts.slice(0, -1).join("."); 88 | const outputFileName = baseName.replace(/^.*[\\/]/, "") + ".js"; 89 | console.log(`✅ Compiled ${fileName} to ${outputFileName}`); 90 | console.log(`🚀 Run with: node ${outputFileName}`); 91 | writeFileSync(outputFileName, output); 92 | // console.log(`Running ${outputFileName}...`); 93 | // eval(output); -------------------------------------------------------------------------------- /myprogram.ay: -------------------------------------------------------------------------------- 1 | l a = "my program variables are nicely scoped"; 2 | l b = "hello world" 3 | l c = 6 + 3 4 | 5 | l d = round(rand() * 12) 6 | l trueVar = true 7 | l falseVar = false 8 | l arr = [ 9 | 1, 10 | 3, 11 | 5, 12 | 7, 13 | ] 14 | 15 | /* 16 | AY Language - Comprehensive Example Program 17 | This file demonstrates all major features of the AY programming language 18 | 19 | === Variable Declarations === 20 | Variables are declared with 'l' keyword and are nicely scoped 21 | 22 | === Comments Support === 23 | */ 24 | /*ignored hopefully*/ 25 | /* 26 | This too is ignored 27 | Multi-line comments work perfectly 28 | */ 29 | 30 | f add(a,b){ 31 | l c = a + /*yooooo*/ b 32 | return c 33 | } 34 | 35 | f greet(name) { 36 | l greeting = "Hello, " + name + "!" 37 | return greeting 38 | } 39 | 40 | l userName = "Alice" 41 | l welcomeMessage = greet(userName) 42 | print(welcomeMessage) 43 | 44 | f factorial(n) { 45 | if (n <= 1) { 46 | return 1 47 | } 48 | return n * factorial(n - 1) 49 | } 50 | 51 | f fibonacci(n) { 52 | if (n <= 1) { 53 | return n 54 | } 55 | return fibonacci(n - 1) + fibonacci(n - 2) 56 | } 57 | 58 | l factResult = factorial(5) 59 | l fibResult = fibonacci(8) 60 | print(factResult) 61 | print(fibResult) 62 | 63 | f foo(a) { 64 | if (a > 0) { 65 | l result = add(a, a); 66 | return result; 67 | } 68 | } 69 | 70 | l i = 0 71 | while ( i < 5){ 72 | print(i) 73 | i++ 74 | } 75 | 76 | for (l i = 0; i < 8; i++){ 77 | print(i); 78 | } 79 | 80 | l doubleResult = foo(20) 81 | print(doubleResult) 82 | 83 | f randPrint(){ 84 | if (d > 6){ 85 | l comparison = 0.5 < d 86 | print(comparison) 87 | print(d) 88 | }else{ 89 | print(d) 90 | } 91 | } 92 | randPrint() 93 | 94 | l testingVar 95 | 96 | def var -> l 97 | def fn -> f 98 | def brk -> break 99 | def cnt -> continue 100 | 101 | var aliasedVariable = "This was declared using var alias!" 102 | print(aliasedVariable) 103 | 104 | fn aliasedFunction(x, y) { 105 | var sum = x + y 106 | return sum 107 | } 108 | 109 | var aliasResult = aliasedFunction(10, 15) 110 | print(aliasResult) 111 | 112 | var counter = 0 113 | while (counter < 10) { 114 | print(counter) 115 | counter++ 116 | if (counter == 3) { 117 | brk 118 | } 119 | } 120 | 121 | l numbers = [1, 2, 3, 4, 5] 122 | 123 | print(numbers, len(numbers)) 124 | 125 | /*l promise = httpGet("https://restcountries.com/v3.1/all?fields=name,flags"); 126 | 127 | f onErr(e){ 128 | print(e) 129 | } 130 | 131 | awaitPromise(promise, f (res){ 132 | print(res) 133 | }, onErr)*/ 134 | 135 | l complexCalc = factorial(4) + fibonacci(6) 136 | print(complexCalc) 137 | 138 | l mathResult = add(factorial(3), fibonacci(5)) 139 | print(mathResult) 140 | 141 | l asks = input("WHat you gonna type ei? ") 142 | print(asks, len(asks)) 143 | l numberP = numbers[randInt(0,4)] 144 | print(numberP) 145 | 146 | while(true){ 147 | writestdout(0) 148 | break 149 | } 150 | writestdout("\n") 151 | writestdout("Hey ") 152 | writestdout("World\n") 153 | l addComp = 8 + 9 - (7/6*8) -------------------------------------------------------------------------------- /myprogram.js: -------------------------------------------------------------------------------- 1 | 2 | function sort(arr, compareFn) { 3 | if (!Array.isArray(arr)) { 4 | console.error('Input must be an array'); 5 | process.exit(1) 6 | } 7 | if (!compareFn) { 8 | return arr.sort(); 9 | } else { 10 | return arr.sort(compareFn); 11 | } 12 | } 13 | 14 | function reverse(arr) { 15 | if (!Array.isArray(arr)) { 16 | console.error('Input must be an array'); 17 | process.exit(1) 18 | } 19 | return arr.reverse(); 20 | } 21 | 22 | function filter(arr, callback) { 23 | if (!Array.isArray(arr)) { 24 | console.error('Input must be an array'); 25 | process.exit(1) 26 | } 27 | return arr.filter(callback); 28 | } 29 | 30 | function map(arr, callback) { 31 | if (!Array.isArray(arr)) { 32 | console.error('Input must be an array'); 33 | process.exit(1) 34 | } 35 | return arr.map(callback); 36 | } 37 | 38 | function slice(arr, start, end) { 39 | if (!Array.isArray(arr)) { 40 | console.error('Input must be an array'); 41 | process.exit(1) 42 | } 43 | return arr.slice(start, end); 44 | } 45 | 46 | function splice(arr, start, deleteCount, ...items) { 47 | if (!Array.isArray(arr)) { 48 | console.error('Input must be an array'); 49 | process.exit(1) 50 | } 51 | arr.splice(start, deleteCount,...items); 52 | return arr; 53 | } 54 | 55 | function push(arr,...items) { 56 | if (!Array.isArray(arr)) { 57 | console.error('Input must be an array'); 58 | process.exit(1) 59 | } 60 | arr.push(...items); 61 | return arr; 62 | } 63 | 64 | function pop(arr) { 65 | if (!Array.isArray(arr)) { 66 | console.error('Input must be an array but got,', typeof arr, 'instead for this function'); 67 | process.exit(1) 68 | } 69 | arr.pop(); 70 | return arr; 71 | } 72 | 73 | function len(arr) { 74 | if (!Array.isArray(arr) && typeof arr !== "string") { 75 | console.error('Input must be an array or string but got,', typeof arr, 'instead for this function'); 76 | process.exit(1) 77 | } 78 | return arr.length; 79 | } 80 | 81 | function newArr(arr, size, fillValue = null){ 82 | if (!Array.isArray(arr)) { 83 | console.error('Input must be an array'); 84 | process.exit(1) 85 | } 86 | return Array.from({ length: size }, (_, i) => arr[i] || fillValue); 87 | } 88 | function includes(arr, value) { 89 | if (!Array.isArray(arr) && typeof arr !== "string") { 90 | console.error('Input must be an array or string but got,', typeof arr, 'instead for this function'); 91 | process.exit(1) 92 | } 93 | return arr.includes(value); 94 | } 95 | function rand(min = 0, max = 0) { 96 | return Math.random() * (max - min + 1) + min 97 | } 98 | 99 | function randInt(min = 0, max = 0) { 100 | return Math.floor(Math.random() * (max - min + 1) + min) 101 | } 102 | 103 | function round(num, precision = 0) { 104 | return Math.round(num * Math.pow(10, precision)) / Math.pow(10, precision); 105 | } 106 | 107 | // Basic trigonometric functions 108 | function sin(x) { 109 | return Math.sin(x); 110 | } 111 | 112 | function cos(x) { 113 | return Math.cos(x); 114 | } 115 | 116 | function tan(x) { 117 | return Math.tan(x); 118 | } 119 | 120 | // Inverse trigonometric functions 121 | function asin(x) { 122 | return Math.asin(x); 123 | } 124 | 125 | function acos(x) { 126 | return Math.acos(x); 127 | } 128 | 129 | function atan(x) { 130 | return Math.atan(x); 131 | } 132 | 133 | function atan2(y, x) { 134 | return Math.atan2(y, x); 135 | } 136 | 137 | // Hyperbolic functions 138 | function sinh(x) { 139 | return Math.sinh(x); 140 | } 141 | 142 | function cosh(x) { 143 | return Math.cosh(x); 144 | } 145 | 146 | function tanh(x) { 147 | return Math.tanh(x); 148 | } 149 | 150 | // Inverse hyperbolic functions 151 | function asinh(x) { 152 | return Math.asinh(x); 153 | } 154 | 155 | function acosh(x) { 156 | return Math.acosh(x); 157 | } 158 | 159 | function atanh(x) { 160 | return Math.atanh(x); 161 | } 162 | 163 | // Trigonometric identities and utility functions 164 | function sec(x) { 165 | return 1 / Math.cos(x); 166 | } 167 | 168 | function csc(x) { 169 | return 1 / Math.sin(x); 170 | } 171 | 172 | function cot(x) { 173 | return 1 / Math.tan(x); 174 | } 175 | 176 | // Convert between degrees and radians 177 | function toRadians(degrees) { 178 | return degrees * (Math.PI / 180); 179 | } 180 | 181 | function toDegrees(radians) { 182 | return radians * (180 / Math.PI); 183 | } 184 | 185 | // Degree-based trigonometric functions 186 | function sind(degrees) { 187 | return Math.sin(toRadians(degrees)); 188 | } 189 | 190 | function cosd(degrees) { 191 | return Math.cos(toRadians(degrees)); 192 | } 193 | 194 | function tand(degrees) { 195 | return Math.tan(toRadians(degrees)); 196 | } 197 | 198 | // Additional math constants and functions 199 | function pi() { 200 | return Math.PI; 201 | } 202 | 203 | function e() { 204 | return Math.E; 205 | } 206 | 207 | function abs(x) { 208 | return Math.abs(x); 209 | } 210 | 211 | function sqrt(x) { 212 | return Math.sqrt(x); 213 | } 214 | 215 | function pow(base, exponent) { 216 | return Math.pow(base, exponent); 217 | } 218 | 219 | function exp(x) { 220 | return Math.exp(x); 221 | } 222 | 223 | function log(x) { 224 | return Math.log(x); 225 | } 226 | 227 | function log10(x) { 228 | return Math.log10(x); 229 | } 230 | 231 | function log2(x) { 232 | return Math.log2(x); 233 | } 234 | 235 | function floor(x) { 236 | return Math.floor(x); 237 | } 238 | 239 | function ceil(x) { 240 | return Math.ceil(x); 241 | } 242 | 243 | function max(...numbers) { 244 | return Math.max(...numbers); 245 | } 246 | 247 | function min(...numbers) { 248 | return Math.min(...numbers); 249 | } 250 | 251 | // String utility functions for AY language 252 | function split(str, delimiter) { 253 | return str.split(delimiter); 254 | } 255 | 256 | function reverse(str) { 257 | return str.split("").reverse().join(""); 258 | } 259 | 260 | function join(arr, delimiter) { 261 | return arr.join(delimiter); 262 | } 263 | 264 | function upper(str) { 265 | return str.toUpperCase(); 266 | } 267 | 268 | function lower(str) { 269 | return str.toLowerCase(); 270 | } 271 | // Print utility functions for AY language 272 | function coolPrint(msg) { 273 | console.log("[COOL PRINT]", msg); 274 | } 275 | 276 | function fancyLog(msg) { 277 | console.log("✨ FANCY LOG:", msg); 278 | } 279 | 280 | function stylishWarn(msg) { 281 | console.warn("⚠️ STYLISH WARNING:", msg); 282 | } 283 | 284 | function errorPop(msg) { 285 | console.error("❌ ERROR POP:", msg); 286 | } 287 | 288 | function print(...msg) { 289 | console.log(...msg); 290 | } 291 | 292 | function errorlog(...msg) { 293 | console.error(...msg); 294 | } 295 | 296 | 297 | // Synchronous input function using process.stdin (blocks execution) 298 | const fs = require("fs"); 299 | 300 | function input(prompt = "") { 301 | if (prompt) process.stdout.write(prompt); 302 | 303 | // Read from stdin until newline 304 | const buffer = Buffer.alloc(1024); 305 | const bytesRead = fs.readSync(process.stdin.fd, buffer, 0, buffer.length, null); 306 | 307 | return buffer.toString("utf8", 0, bytesRead).trim(); 308 | } 309 | 310 | function writestdout(...args){ 311 | process.stdout.write(args.join(' ')); 312 | } 313 | const { readFileSync, writeFileSync } = require("node:fs"); 314 | 315 | function read(path, options = "utf-8"){ 316 | return readFileSync(path, options) 317 | } 318 | 319 | function write(file, data){ 320 | return writeFileSync(file, data); 321 | } 322 | // Basic date utility functions 323 | const dateToISO = (date) => { 324 | const d = date instanceof Date ? date : new Date(date); 325 | return d.toISOString(); 326 | }; 327 | 328 | const dateToLocal = (date) => { 329 | const d = date instanceof Date ? date : new Date(date); 330 | return d.toLocaleString(); 331 | }; 332 | 333 | const dateToShort = (date) => { 334 | const d = date instanceof Date ? date : new Date(date); 335 | return `${d.getMonth() + 1}/${d.getDate()}/${d.getFullYear()}`; 336 | }; 337 | 338 | const dateToLong = (date) => { 339 | const d = date instanceof Date ? date : new Date(date); 340 | return d.toLocaleDateString('en-US', { 341 | weekday: 'long', 342 | year: 'numeric', 343 | month: 'long', 344 | day: 'numeric' 345 | }); 346 | }; 347 | 348 | const dateDiffInDays = (date1, date2) => { 349 | const d1 = new Date(date1); 350 | const d2 = new Date(date2); 351 | const diffTime = Math.abs(d2 - d1); 352 | return Math.ceil(diffTime / (1000 * 60 * 60 * 24)); 353 | }; 354 | 355 | const dateDiffInHours = (date1, date2) => { 356 | const d1 = new Date(date1); 357 | const d2 = new Date(date2); 358 | const diffTime = Math.abs(d2 - d1); 359 | return Math.ceil(diffTime / (1000 * 60 * 60)); 360 | }; 361 | 362 | const dateDiffInMinutes = (date1, date2) => { 363 | const d1 = new Date(date1); 364 | const d2 = new Date(date2); 365 | const diffTime = Math.abs(d2 - d1); 366 | return Math.ceil(diffTime / (1000 * 60)); 367 | }; 368 | 369 | const dateDiffInSeconds = (date1, date2) => { 370 | const d1 = new Date(date1); 371 | const d2 = new Date(date2); 372 | const diffTime = Math.abs(d2 - d1); 373 | return Math.ceil(diffTime / 1000); 374 | }; 375 | 376 | // Advanced date manipulation 377 | const dateAdd = (date, value, unit) => { 378 | const d = new Date(date); 379 | switch(unit.toLowerCase()) { 380 | case 'years': d.setFullYear(d.getFullYear() + value); break; 381 | case 'months': d.setMonth(d.getMonth() + value); break; 382 | case 'days': d.setDate(d.getDate() + value); break; 383 | case 'hours': d.setHours(d.getHours() + value); break; 384 | case 'minutes': d.setMinutes(d.getMinutes() + value); break; 385 | case 'seconds': d.setSeconds(d.getSeconds() + value); break; 386 | } 387 | return d; 388 | }; 389 | 390 | const dateSubtract = (date, value, unit) => { 391 | return dateAdd(date, -value, unit); 392 | }; 393 | 394 | const dateStartOf = (date, unit) => { 395 | const d = new Date(date); 396 | switch(unit.toLowerCase()) { 397 | case 'year': d.setMonth(0, 1); d.setHours(0, 0, 0, 0); break; 398 | case 'month': d.setDate(1); d.setHours(0, 0, 0, 0); break; 399 | case 'day': d.setHours(0, 0, 0, 0); break; 400 | case 'hour': d.setMinutes(0, 0, 0); break; 401 | } 402 | return d; 403 | }; 404 | 405 | const dateEndOf = (date, unit) => { 406 | const d = new Date(date); 407 | switch(unit.toLowerCase()) { 408 | case 'year': d.setMonth(11, 31); d.setHours(23, 59, 59, 999); break; 409 | case 'month': d.setMonth(d.getMonth() + 1, 0); d.setHours(23, 59, 59, 999); break; 410 | case 'day': d.setHours(23, 59, 59, 999); break; 411 | case 'hour': d.setMinutes(59, 59, 999); break; 412 | } 413 | return d; 414 | }; 415 | 416 | // Helper function to check if date is valid 417 | const isValidDateFormat = (date) => { 418 | const d = new Date(date); 419 | return d instanceof Date && !isNaN(d); 420 | }; 421 | 422 | function now(){ 423 | return new Date(); 424 | } 425 | function timestamp(){ 426 | return Date.now(); 427 | } 428 | // Timer utility functions for AY language 429 | function Timeout(fn, delay) { 430 | return setTimeout(fn, delay); 431 | } 432 | 433 | function Interval(fn, interval) { 434 | return setInterval(fn, interval); 435 | } 436 | 437 | function stopTimeout(timeoutId) { 438 | clearTimeout(timeoutId); 439 | } 440 | 441 | function stopInterval(intervalId) { 442 | clearInterval(intervalId); 443 | } 444 | let a = "my program variables are nicely scoped"; 445 | let b = "hello world"; 446 | let c = 6 + 3; 447 | let d = round(rand() * 12); 448 | let trueVar = true; 449 | let falseVar = false; 450 | let arr = [1, 3, 5, 7]; 451 | function add(a, b) { 452 | let c = a + b; 453 | return c; 454 | } 455 | function greet(name) { 456 | let greeting = "Hello, " + name + "!"; 457 | return greeting; 458 | } 459 | let userName = "Alice"; 460 | let welcomeMessage = greet(userName); 461 | print(welcomeMessage) 462 | function factorial(n) { 463 | if ((n <= 1)) { 464 | return 1; 465 | } 466 | return n * factorial(n - 1); 467 | } 468 | function fibonacci(n) { 469 | if ((n <= 1)) { 470 | return n; 471 | } 472 | return fibonacci(n - 1) + fibonacci(n - 2); 473 | } 474 | let factResult = factorial(5); 475 | let fibResult = fibonacci(8); 476 | print(factResult) 477 | print(fibResult) 478 | function foo(a) { 479 | if ((a > 0)) { 480 | let result = add(a, a); 481 | return result; 482 | } 483 | } 484 | let i = 0; 485 | while (i < 5) { 486 | print(i) 487 | i++; 488 | } 489 | for (let i = 0; (i < 8); i++) { 490 | print(i) 491 | } 492 | let doubleResult = foo(20); 493 | print(doubleResult) 494 | function randPrint() { 495 | if ((d > 6)) { 496 | let comparison = 0.5 < d; 497 | print(comparison) 498 | print(d) 499 | } else { 500 | print(d) 501 | } 502 | } 503 | randPrint() 504 | let testingVar; 505 | 506 | 507 | 508 | 509 | let aliasedVariable = "This was declared using var alias!"; 510 | print(aliasedVariable) 511 | function aliasedFunction(x, y) { 512 | let sum = x + y; 513 | return sum; 514 | } 515 | let aliasResult = aliasedFunction(10, 15); 516 | print(aliasResult) 517 | let counter = 0; 518 | while (counter < 10) { 519 | print(counter) 520 | counter++; 521 | if ((counter == 3)) { 522 | break; 523 | } 524 | } 525 | let numbers = [1, 2, 3, 4, 5]; 526 | print(numbers, len(numbers)) 527 | let complexCalc = factorial(4) + fibonacci(6); 528 | print(complexCalc) 529 | let mathResult = add(factorial(3), fibonacci(5)); 530 | print(mathResult) 531 | let asks = input("WHat you gonna type ei? "); 532 | print(asks, len(asks)) 533 | let numberP = numbers[randInt(0, 4)]; 534 | print(numberP) 535 | while (true) { 536 | writestdout(0) 537 | break; 538 | } 539 | writestdout("\n") 540 | writestdout("Hey ") 541 | writestdout("World\n") 542 | let addComp = 8 + 9 - (7 / 6 * 8); 543 | // Functional HTTP utilities for AY language 544 | // All functions are pure and functional - no side effects, immutable data 545 | 546 | // Simple GET Request 547 | function httpGet(url, parseType = "json") { 548 | return fetch(url) 549 | .then((response) => { 550 | if (parseType === "json") { 551 | return response.json(); 552 | } else if (parseType === "text") { 553 | return response.text(); 554 | } else if (parseType === "blob") { 555 | return response.blob(); 556 | } else if (parseType === "arrayBuffer") { 557 | return response.arrayBuffer(); 558 | } else if (parseType === "formData") { 559 | return response.formData(); 560 | } else { 561 | // Default to json if unknown type 562 | return response.json(); 563 | } 564 | }) 565 | .catch((error) => ({ error: error.message, success: false })); 566 | } 567 | 568 | // Simple POST Request 569 | function httpPost(url, data) { 570 | return fetch(url, { 571 | method: "POST", 572 | headers: { "Content-Type": "application/json" }, 573 | body: JSON.stringify(data), 574 | }) 575 | .then((response) => response.json()) 576 | .catch((error) => ({ error: error.message, success: false })); 577 | } 578 | 579 | // Simple PUT Request 580 | function httpPut(url, data) { 581 | return fetch(url, { 582 | method: "PUT", 583 | headers: { "Content-Type": "application/json" }, 584 | body: JSON.stringify(data), 585 | }) 586 | .then((response) => response.json()) 587 | .catch((error) => ({ error: error.message, success: false })); 588 | } 589 | 590 | // Simple DELETE Request 591 | function httpDelete(url) { 592 | return fetch(url, { 593 | method: "DELETE", 594 | }) 595 | .then((response) => response.json()) 596 | .catch((error) => ({ error: error.message, success: false })); 597 | } 598 | 599 | // Create HTTP Server 600 | function createHttpServer(port) { 601 | const http = require("http"); 602 | const url = require("url"); 603 | 604 | const server = http.createServer((req, res) => { 605 | res.setHeader("Access-Control-Allow-Origin", "*"); 606 | res.setHeader( 607 | "Access-Control-Allow-Methods", 608 | "GET, POST, PUT, DELETE, OPTIONS" 609 | ); 610 | res.setHeader( 611 | "Access-Control-Allow-Headers", 612 | "Content-Type, Authorization" 613 | ); 614 | 615 | if (req.method === "OPTIONS") { 616 | res.writeHead(200); 617 | res.end(); 618 | return; 619 | } 620 | 621 | const parsedUrl = url.parse(req.url, true); 622 | const path = parsedUrl.pathname; 623 | const method = req.method; 624 | 625 | let body = ""; 626 | req.on("data", (chunk) => { 627 | body += chunk.toString(); 628 | }); 629 | 630 | req.on("end", () => { 631 | const response = { 632 | message: "Hello from AY HTTP Server!", 633 | method: method, 634 | path: path, 635 | query: parsedUrl.query, 636 | timestamp: new Date().toISOString(), 637 | }; 638 | 639 | res.writeHead(200, { "Content-Type": "application/json" }); 640 | res.end(JSON.stringify(response)); 641 | }); 642 | }); 643 | 644 | server.listen(port, () => { 645 | console.log(`AY HTTP Server running on port ${port}`); 646 | }); 647 | 648 | return server; 649 | } 650 | 651 | // Start Server 652 | function startHttpServer(port) { 653 | return createHttpServer(port); 654 | } 655 | 656 | // Stop Server 657 | function stopHttpServer(server) { 658 | if (server && server.close) { 659 | server.close(); 660 | return true; 661 | } 662 | return false; 663 | } 664 | 665 | // JSON Response Helper 666 | function createJsonResponse(data, status) { 667 | return { 668 | status: status || 200, 669 | data: data, 670 | timestamp: new Date().toISOString(), 671 | }; 672 | } 673 | 674 | // Error Response Helper 675 | function createErrorResponse(message, status) { 676 | return { 677 | status: status || 500, 678 | error: message, 679 | timestamp: new Date().toISOString(), 680 | }; 681 | } 682 | 683 | // Success Response Helper 684 | function createSuccessResponse(data, message) { 685 | return { 686 | status: 200, 687 | success: true, 688 | message: message || "Success", 689 | data: data, 690 | timestamp: new Date().toISOString(), 691 | }; 692 | } 693 | 694 | // URL Builder 695 | function buildHttpUrl(base, path) { 696 | return base + path; 697 | } 698 | 699 | // Query String Builder 700 | function buildQueryString(params) { 701 | const query = new URLSearchParams(); 702 | for (const key in params) { 703 | query.append(key, params[key]); 704 | } 705 | return query.toString(); 706 | } 707 | 708 | // Parse JSON 709 | function parseJson(jsonString) { 710 | try { 711 | return JSON.parse(jsonString); 712 | } catch (error) { 713 | return { error: "Invalid JSON", success: false }; 714 | } 715 | } 716 | 717 | // Stringify JSON 718 | function stringifyJson(obj) { 719 | try { 720 | return JSON.stringify(obj); 721 | } catch (error) { 722 | return "{}"; 723 | } 724 | } 725 | 726 | // HTTP Status Helper 727 | function getHttpStatusMessage(status) { 728 | const statusMessages = { 729 | 200: "OK", 730 | 201: "Created", 731 | 400: "Bad Request", 732 | 401: "Unauthorized", 733 | 403: "Forbidden", 734 | 404: "Not Found", 735 | 500: "Internal Server Error", 736 | }; 737 | return statusMessages[status] || "Unknown Status"; 738 | } 739 | 740 | 741 | 742 | // HTTP Logger 743 | function logHttpRequest(method, url, data) { 744 | const timestamp = new Date().toISOString(); 745 | console.log(`[${timestamp}] ${method} ${url}`); 746 | if (data) { 747 | console.log("Data:", JSON.stringify(data, null, 2)); 748 | } 749 | } 750 | 751 | // HTTP Logger for Response 752 | function logHttpResponse(response) { 753 | const timestamp = new Date().toISOString(); 754 | console.log(`[${timestamp}] Response:`, JSON.stringify(response, null, 2)); 755 | } 756 | 757 | // Promise Resolution Utilities - Functional approach 758 | 759 | // Simple promise resolver - waits for promise and returns result 760 | function awaitPromise(promise, onSuccess, onError) { 761 | return promise.then(onSuccess).catch(onError); 762 | } 763 | 764 | // Promise resolver with timeout 765 | function awaitPromiseWithTimeout(promise, onSuccess, onError, timeout) { 766 | return Promise.race([ 767 | promise, 768 | new Promise((_, reject) => 769 | setTimeout(() => reject(new Error("Promise timeout")), timeout || 5000) 770 | ), 771 | ]) 772 | .then(onSuccess) 773 | .catch(onError); 774 | } 775 | 776 | 777 | 778 | // Promise chain helper - run multiple promises 779 | function awaitAll(promises, onSuccess, onError) { 780 | return Promise.all(promises).then(onSuccess).catch(onError); 781 | } 782 | 783 | 784 | 785 | 786 | // Simple promise logger 787 | function logPromise(promise, label, varName) { 788 | console.log(`Starting promise: ${label || "Unnamed"}`); 789 | return promise 790 | .then((result) => { 791 | console.log(`Promise resolved: ${label || "Unnamed"}`, result); 792 | varName = result; 793 | return result; 794 | }) 795 | .catch((error) => { 796 | console.error(`Promise rejected: ${label || "Unnamed"}`, error.message); 797 | return { error: error.message, success: false }; 798 | }); 799 | } 800 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ayscript", 3 | "version": "1.0.3", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "ayscript", 9 | "version": "1.0.3", 10 | "license": "ISC", 11 | "dependencies": { 12 | "@types/node": "^20.11.17", 13 | "typescript": "^5.3.3" 14 | }, 15 | "bin": { 16 | "ayc": "dist/index.js" 17 | }, 18 | "devDependencies": {} 19 | }, 20 | "node_modules/@types/node": { 21 | "version": "20.11.17", 22 | "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.17.tgz", 23 | "integrity": "sha512-QmgQZGWu1Yw9TDyAP9ZzpFJKynYNeOvwMJmaxABfieQoVoiVOS6MN1WSpqpRcbeA5+RW82kraAVxCCJg+780Qw==", 24 | "dependencies": { 25 | "undici-types": "~5.26.4" 26 | } 27 | }, 28 | "node_modules/typescript": { 29 | "version": "5.3.3", 30 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", 31 | "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", 32 | "bin": { 33 | "tsc": "bin/tsc", 34 | "tsserver": "bin/tsserver" 35 | }, 36 | "engines": { 37 | "node": ">=14.17" 38 | } 39 | }, 40 | "node_modules/undici-types": { 41 | "version": "5.26.5", 42 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", 43 | "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@types/node": "^20.11.17", 4 | "typescript": "^5.3.3" 5 | }, 6 | "name": "ayscript", 7 | "version": "1.0.335", 8 | "main": "./dist/index.js", 9 | "files": [ 10 | "dist/", 11 | "functions/", 12 | "README.md" 13 | ], 14 | "devDependencies": {}, 15 | "scripts": { 16 | "build": "npx tsc", 17 | "prepublishOnly": "npm run build", 18 | "test": "echo \"Error: no test specified\" && exit 1" 19 | }, 20 | "bin":{ 21 | "ayc" : "./dist/index.js" 22 | }, 23 | "keywords": [ 24 | "AY", 25 | "Transpiler", 26 | "nodejs", 27 | "Typescript", 28 | "Lazy language" 29 | ], 30 | "author": "Oluwatola Ayomide, CHO inc", 31 | "license": "ISC", 32 | "description": "A compiler, for my AY language, built with Typescript and Nodejs" 33 | } 34 | -------------------------------------------------------------------------------- /parser/astcompiler.ts: -------------------------------------------------------------------------------- 1 | import { ASTNode, ASTNodeType } from "./asts"; 2 | import { Parser } from "./parser"; 3 | // AST to JavaScript compiler for AY language 4 | // This file exports a function that takes an AST (array of nodes) and returns JavaScript code as a string 5 | 6 | export default function compileAST(ast:ASTNode[]) { 7 | function compileNode(node) { 8 | if (!node) return ""; 9 | switch (node.type) { 10 | case ASTNodeType.VariableDeclaration: 11 | if (node.initializer) { 12 | return `let ${node.identifier} = ${compileNode(node.initializer)};`; 13 | } else { 14 | return `let ${node.identifier};`; 15 | } 16 | case ASTNodeType.Literal: 17 | return node.value; 18 | case ASTNodeType.FunctionDeclaration: 19 | return `function ${node.identifier || ""}(${(node.params||[]).map(compileNode).join(", ")}) {\n${(node.body||[]).map(compileNode).join("\n")}\n}`; 20 | case ASTNodeType.Return: 21 | if (node.initializer) { 22 | return `return ${compileNode(node.initializer)};`; 23 | } else if (node.value) { 24 | return `return ${node.value};`; 25 | } else { 26 | return "return;"; 27 | } 28 | case ASTNodeType.Break: 29 | return "break;"; 30 | case ASTNodeType.Continue: 31 | return "continue;"; 32 | case ASTNodeType.IfElse: 33 | return compileIfElse(node); 34 | case ASTNodeType.Loop: 35 | return compileLoop(node); 36 | case "CallExpression": 37 | return `${node.identifier}(${(node.args||[]).map(compileNode).join(", ")})`; 38 | default: 39 | // Binary, Unary, Array, etc. 40 | if (node.operator && node.left !== undefined && node.right !== undefined) { 41 | // Handle string concatenation and other binary operations 42 | const left = compileNode(node.left); 43 | let right; 44 | if (node.right && node.right.paren) { 45 | right = compileTest(node.right); 46 | } else { 47 | right = compileNode(node.right); 48 | } 49 | return `${left} ${node.operator} ${right}`; 50 | } 51 | if (node.postop && node.identifier) { 52 | return `${node.identifier}${node.postop};`; 53 | } 54 | if (node.infixop && node.identifier) { 55 | return `${node.infixop} ${node.identifier};`; 56 | } 57 | if (node.elements) { 58 | return `[${node.elements.map(compileNode).join(", ")}]`; 59 | } 60 | if (node.identifier && node.index) { 61 | if (Array.isArray(node.index)) { 62 | return `${node.identifier}[${node.index.map(compileNode).join("][")}]`; 63 | } else { 64 | return `${node.identifier}[${compileNode(node.index)}]`; 65 | } 66 | } 67 | if (typeof node === "string") { 68 | return node; 69 | } 70 | return ""; 71 | } 72 | } 73 | 74 | function compileIfElse(node) { 75 | const test = node.test ? compileTest(node.test) : ""; 76 | const cons = Array.isArray(node.consequence) ? node.consequence : [node.consequence]; 77 | const alt = node.alternate; 78 | let code = `if (${test}) {\n${cons.map(compileNode).join("\n")}\n}`; 79 | if (alt) { 80 | if (Array.isArray(alt)) { 81 | code += ` else {\n${alt.map(compileNode).join("\n")}\n}`; 82 | } else { 83 | code += ` else ${compileIfElse(alt)}`; 84 | } 85 | } 86 | return code; 87 | } 88 | 89 | function compileTest(test) { 90 | if (test.paren && test.paren.left && test.paren.operator && test.paren.right) { 91 | return `(${compileNode(test.paren.left)} ${test.paren.operator} ${compileNode(test.paren.right)})`; 92 | } 93 | if (test.operator && test.left !== undefined && test.right !== undefined) { 94 | return `(${compileNode(test.left)} ${test.operator} ${compileNode(test.right)})`; 95 | } 96 | // Bool usually comes like { paren: "true" } or { paren: "false" } 97 | if (typeof test.paren === "string") { 98 | return`(${test.paren})`; 99 | } 100 | return compileNode(test); 101 | } 102 | 103 | function compileLoop(node) { 104 | // For loop 105 | if (node.initializer && node.test && node.upgrade) { 106 | return `for (${compileNode(node.initializer)} ${compileTest(node.test)}; ${compileNode(node.upgrade).slice(0, -1)}) {\n${(node.body||[]).map(compileNode).join("\n")}\n}`; 107 | } 108 | // While loop 109 | if (node.test && node.body) { 110 | return `while ${compileTest(node.test)} {\n${(node.body||[]).map(compileNode).join("\n")}\n}`; 111 | } 112 | return ""; 113 | } 114 | 115 | return ast.map(compileNode).join("\n"); 116 | } 117 | // let f = Bun.file("./test.ay"); 118 | // const p = new Parser(await f.text()); 119 | // p.start(); 120 | // console.log(compileAST(p.nodes)); 121 | -------------------------------------------------------------------------------- /parser/asts.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | export enum ASTNodeType { 4 | Program, 5 | VariableDeclaration, 6 | Expression, 7 | Literal, 8 | Identifier, 9 | TernaryExpression, 10 | BinaryExpression, 11 | UnaryExpression, 12 | FunctionDeclaration, 13 | DefDecl, 14 | BlockStatement, 15 | Return, 16 | IfElse, 17 | Loop, 18 | Break, 19 | Continue 20 | } 21 | type right = { 22 | type?:string; 23 | name?:string; 24 | operator?:string; 25 | left?:{ 26 | type?:string; 27 | value?:string 28 | }; 29 | right?:right 30 | } 31 | type left = { 32 | type?:string; 33 | value?:string 34 | } 35 | type init = { 36 | type?:string; 37 | value?:string; 38 | raw?:string 39 | } 40 | type params = left[] 41 | type BinaryExpressionNode = { 42 | type: ASTNodeType; 43 | operator: string; 44 | left: ASTNode; 45 | right: ASTNode; 46 | }; 47 | type VariableDeclarationNode = { 48 | type: ASTNodeType; 49 | identifier: string; 50 | initializer: ASTNode; // Initializer (could be a literal or expression) 51 | }; 52 | 53 | type ReturnNode = { 54 | type:ASTNodeType, 55 | initializer?:ASTNode, 56 | value?:string 57 | } 58 | 59 | type IfElse = { 60 | //if-else 61 | type:ASTNodeType, 62 | test?:ASTNode; 63 | consequent?:ASTNode; 64 | alternate?:ASTNode 65 | } 66 | //i plan on using this to represent any kind of node at all, due to lack of proper typescript knowledge in dealing with seperation of concerns 67 | export interface ASTNode { 68 | // base of literals and identifiers 69 | type: ASTNodeType; 70 | name?:string; 71 | value?:string; 72 | raw?:string; 73 | identifier?:string; 74 | initializer?:ASTNode | null; 75 | dataType?:string; 76 | // expression mostly 77 | operator?:string; 78 | left?:left; 79 | right?:right; 80 | // i guess in variables 81 | init?:init; 82 | // block statements && functions 83 | body?:ASTNode; 84 | //functions 85 | params?:params; 86 | //if-else 87 | test?:ASTNode; 88 | consequent?:ASTNode; 89 | alternate?:ASTNode 90 | index?:ASTNode | ASTNode[]; 91 | } 92 | export interface Variable{ 93 | dataType: string, 94 | val:string, 95 | nodePos:number 96 | } 97 | -------------------------------------------------------------------------------- /parser/parser.ts: -------------------------------------------------------------------------------- 1 | import { ASTNode, ASTNodeType, Variable } from "./asts"; 2 | import { isAllowedKey, TokenGen, tokens, TokenType } from "./tokens"; 3 | 4 | export class Parser { 5 | defines: Map; 6 | private tokenizer: TokenGen; 7 | nodes: ASTNode[]; 8 | parens: string[]; 9 | braces: string[]; 10 | bracs: string[]; 11 | vars: Variable[]; 12 | errors: string[]; 13 | constructor(file: string) { 14 | this.tokenizer = new TokenGen(file); 15 | this.nodes = []; 16 | this.parens = []; 17 | this.braces = []; 18 | this.bracs = []; 19 | this.vars = []; 20 | this.errors = []; 21 | this.defines = new Map(); 22 | } 23 | 24 | private addError(message: string) { 25 | const line = this.tokenizer.getCurrentLineNumber(); 26 | const column = this.tokenizer.getCurrentColumnNumber(); 27 | const currentToken = this.tokenizer.getCurrentToken(); 28 | 29 | // Get the actual source line from the original file 30 | const actualSourceLine = this.tokenizer.lines[line - 1] || "(empty line)"; 31 | 32 | // Create a pointer to show where the error is 33 | const pointer = ' '.repeat(Math.max(0, column - 1)) + '^'; 34 | 35 | const errorMsg = ` 36 | Error at Line ${line}, Column ${column}: ${message} 37 | ${actualSourceLine} 38 | ${pointer} 39 | 40 | Current token: "${currentToken?.value || 'EOF'}" (${currentToken?.type || 'EOF'})`; 41 | 42 | this.errors.push(errorMsg); 43 | } 44 | 45 | // Resolve defined aliases - replace any defined keyword with its actual value 46 | private resolveDefine(value: string): string { 47 | return this.defines.has(value) ? this.defines.get(value)! : value; 48 | } 49 | 50 | consume() { 51 | const token = this.tokenizer.getCurrentToken(); 52 | // Create a new token with resolved value if it's a define 53 | const resolvedToken = { 54 | ...token, 55 | value: this.resolveDefine(token.value) 56 | }; 57 | this.tokenizer.next(); 58 | return resolvedToken; 59 | } 60 | expectPeek(t: TokenType) { 61 | let pk = this.tokenizer.peek(); 62 | if (!pk) { 63 | return false; 64 | } 65 | if (pk.type === t) { 66 | return true; 67 | } else { 68 | return false; 69 | } 70 | } 71 | expectToken(t: TokenType) { 72 | let tk = this.tokenizer.getCurrentToken(); 73 | if (!tk) { 74 | return false; 75 | } 76 | if (tk.type === t) { 77 | return true; 78 | } else { 79 | return false; 80 | } 81 | } 82 | expectPeekVal(v: string) { 83 | let pk = this.tokenizer.peek(); 84 | if (!pk) { 85 | return false; 86 | } 87 | // Check both the original value and the resolved value (for defines) 88 | const resolvedValue = this.resolveDefine(pk.value); 89 | if (pk.value === v || resolvedValue === v) { 90 | return true; 91 | } else { 92 | return false; 93 | } 94 | } 95 | expectTokenVal(v: string) { 96 | let tk = this.tokenizer.getCurrentToken(); 97 | if (!tk) { 98 | return false; 99 | } 100 | // Check both the original value and the resolved value (for defines) 101 | const resolvedValue = this.resolveDefine(tk.value); 102 | if (tk.value === v || resolvedValue === v) { 103 | return true; 104 | } else { 105 | return false; 106 | } 107 | } 108 | parseLiteral(): ASTNode { 109 | const token = this.consume(); 110 | if ( 111 | this.expectToken(TokenType.NewLine) || 112 | this.expectToken(TokenType.EOF) 113 | ) { 114 | this.consume(); 115 | } 116 | return { 117 | type: ASTNodeType.Literal, 118 | value: token.value, 119 | }; 120 | } 121 | isDefinedVar(v: string) { 122 | return this.vars.some((val) => val.val === v); 123 | } 124 | parseNotnMinusExpression() { 125 | const operator = this.consume().value; // Consume the unary operator (! or -) 126 | const operand = this.expectTokenVal("(") 127 | ? this.parseParenExpr() 128 | : this.parseExpression(); 129 | return { 130 | operator, 131 | operand, 132 | }; 133 | } 134 | parseCallExpr() { 135 | const identifier = this.consume().value; // Consume the function identifier 136 | const args: ASTNode[] = []; 137 | 138 | if (!this.expectTokenVal("(")) { 139 | this.addError(`SyntaxError: Expected '(' after function identifier '${identifier}'`); 140 | return null; // Return null if parentheses are not found 141 | } 142 | 143 | this.consume(); // Consume the opening '(' 144 | 145 | // Check for an empty argument list 146 | if (this.expectTokenVal(")")) { 147 | this.consume(); // Consume the closing ')' 148 | return { 149 | type: "CallExpression", 150 | identifier, 151 | args, 152 | }; 153 | } 154 | 155 | // Parse the arguments 156 | while (!this.expectTokenVal(")")) { 157 | const arg = this.parseExpression(); 158 | if (!arg) { 159 | this.addError(`SyntaxError: Invalid argument in function call '${identifier}'`); 160 | break; // Prevent infinite loops if parseExpression fails 161 | } 162 | args.push(arg); // Collect the parsed argument 163 | if (this.expectTokenVal(",")) { 164 | this.consume(); // Consume the comma separator 165 | } else if (!this.expectTokenVal(")")) { 166 | this.addError(`SyntaxError: Expected ',' or ')' in function call '${identifier}'`); 167 | break; 168 | } 169 | } 170 | 171 | if (this.expectTokenVal(")")) { 172 | this.consume(); // Consume the closing ')' 173 | } else { 174 | this.addError(`SyntaxError: Unmatched parentheses in function call '${identifier}'`); 175 | } 176 | 177 | return { 178 | type: "CallExpression", 179 | identifier, 180 | args, 181 | }; 182 | } 183 | parseArray() { 184 | let elements: ASTNode[] = []; 185 | this.consume(); // Consume the opening '[' 186 | 187 | if (this.expectTokenVal("]")) { 188 | this.consume(); 189 | return { elements }; 190 | } 191 | 192 | while (!this.expectTokenVal("]")) { 193 | // Skip newlines 194 | if (this.expectToken(TokenType.NewLine)) { 195 | this.consume(); 196 | continue; 197 | } 198 | 199 | let element = this.parseExpression(); 200 | if (!element) { 201 | this.addError(`SyntaxError: Invalid array element`); 202 | break; 203 | } 204 | elements.push(element); 205 | 206 | if (this.expectTokenVal(",")) { 207 | this.consume(); // Consume the comma separator 208 | } else if (!this.expectTokenVal("]")) { 209 | this.addError(`SyntaxError: Expected ',' or ']' in array`); 210 | break; 211 | } 212 | } 213 | 214 | if (this.expectTokenVal("]")) { 215 | this.consume(); // Consume the closing ']' 216 | } else { 217 | this.addError(`SyntaxError: Unmatched brackets in array`); 218 | } 219 | 220 | return { elements }; 221 | } 222 | parseIncDec() { 223 | if ( 224 | this.expectToken(TokenType.Operator) && 225 | this.expectPeek(TokenType.Identifier) 226 | ) { 227 | let infixop = this.consume().value; 228 | let identifier = this.consume().value; 229 | return { infixop, identifier }; 230 | } else { 231 | let identifier = this.consume().value; 232 | let postop = this.consume().value; 233 | return { 234 | postop, 235 | identifier, 236 | }; 237 | } 238 | } 239 | parseArrIndex(){ 240 | let identifier = this.consume().value; // Consume the array identifier 241 | if (!this.expectTokenVal("[")) { 242 | this.addError(`SyntaxError: Expected '[' after array identifier '${identifier}'`); 243 | return null; 244 | } 245 | 246 | let indexNodes: ASTNode[] = []; 247 | 248 | // Handle multiple nested indices like ident[0][1][2] 249 | while (this.expectTokenVal("[")) { 250 | this.consume(); // Consume the opening '[' 251 | 252 | const index = this.parseExpression(); 253 | if (!index) { 254 | this.addError(`SyntaxError: Invalid array index for '${identifier}'`); 255 | return null; 256 | } 257 | indexNodes.push(index); 258 | 259 | if (!this.expectTokenVal("]")) { 260 | this.addError(`SyntaxError: Expected ']' after array index for '${identifier}'`); 261 | return null; 262 | } 263 | this.consume(); // Consume the closing ']' 264 | } 265 | 266 | // If only one index, return as single node, else as array 267 | const index = indexNodes.length === 1 ? indexNodes[0] : indexNodes; 268 | 269 | return { 270 | identifier, 271 | index, 272 | }; 273 | } 274 | parseExpression() { 275 | let left; 276 | 277 | if (this.expectTokenVal("(")) { 278 | // Handle parenthesized expressions 279 | left = this.parseParenExpr(); 280 | } else if (this.expectTokenVal("!") || this.expectTokenVal("-")) { 281 | left = this.parseNotnMinusExpression(); 282 | } else if ( 283 | this.expectToken(TokenType.Identifier) && 284 | this.expectPeekVal("(") 285 | ) { 286 | left = this.parseCallExpr(); 287 | } else if (this.expectTokenVal("[")) { 288 | left = this.parseArray(); 289 | } else if (this.expectToken(TokenType.Identifier) && this.expectPeekVal("[")) { 290 | left = this.parseArrIndex(); 291 | } else if (this.expectTokenVal("f")) { 292 | left = this.parseFunc(); 293 | } else if ( 294 | this.expectTokenVal("--") || 295 | this.expectTokenVal("++") || 296 | (this.expectToken(TokenType.Identifier) && 297 | (this.expectPeekVal("--") || this.expectPeekVal("++"))) 298 | ) { 299 | left = this.parseIncDec(); 300 | } else { 301 | // Consume basic literals/identifiers 302 | left = this.consume().value; 303 | } 304 | 305 | // Check if there’s an operator next 306 | if (this.expectToken(TokenType.Operator) || this.expectTokenVal("~")) { 307 | if (this.expectTokenVal("~")) { 308 | this.tokenizer.getCurrentToken().value = "+"; 309 | this.tokenizer.getCurrentToken().type = TokenType.Operator; 310 | } 311 | const op = this.consume().value; 312 | // Validate the next token for the right-hand side 313 | if ( 314 | !this.expectToken(TokenType.Identifier) && 315 | !this.expectToken(TokenType.Literal) && 316 | !this.expectToken(TokenType.StringLiteral) && 317 | !this.expectTokenVal("(") 318 | ) { 319 | this.addError(`Invalid expression after operator '${op}' - Expected identifier, number, string, or parenthesized expression`); 320 | return left; // Return what we have so far 321 | } 322 | 323 | // Handle the right-hand side of the expression 324 | let right; 325 | if ( 326 | //Line and file end terminators 327 | this.expectPeek(TokenType.EOF) || 328 | this.expectPeek(TokenType.NewLine) || 329 | this.expectPeekVal(";") || 330 | //parentheses expr 331 | this.expectPeekVal(")") || 332 | //array values and function call exprs end 333 | this.expectPeekVal(",") 334 | ) { 335 | right = this.consume().value; // Simple right-hand expression (now resolved) 336 | if (!this.expectTokenVal(")") && !this.expectTokenVal(",")) { 337 | this.consume(); 338 | } 339 | } else { 340 | right = this.parseExpression(); // Recursively parse complex expressions 341 | } 342 | 343 | return { 344 | operator: op, 345 | left, 346 | right, 347 | }; 348 | } 349 | 350 | return left; // Return single values if no operator is present 351 | } 352 | 353 | parseParenExpr() { 354 | this.consume(); // Consume the opening '(' 355 | // Important Error checks: 356 | if (this.expectTokenVal(")")) { 357 | this.addError("Empty parentheses - Expected an expression inside parentheses"); 358 | } 359 | const expression = this.parseExpression(); // Parse the inner expression 360 | if (this.expectTokenVal(")")) { 361 | this.consume(); // Consume the closing ')' 362 | } else { 363 | this.addError("Unmatched parentheses - Missing closing ')' for opening '('"); 364 | } 365 | 366 | return { paren: expression }; // Return the parsed inner expression 367 | } 368 | 369 | parseVariable() { 370 | this.tokenizer.next(); 371 | let identifier; 372 | let initializer; 373 | let dT = "unknown"; 374 | if (this.expectToken(TokenType.Identifier)) { 375 | identifier = this.consume()?.value; 376 | //this check is used to know whether it's just a plain declaration, without any value initialised in the variable 377 | if ( 378 | this.expectToken(TokenType.EOF) || 379 | this.expectToken(TokenType.NewLine) || 380 | this.expectTokenVal(";") 381 | ) { 382 | this.consume(); 383 | return { 384 | type: ASTNodeType.VariableDeclaration, 385 | identifier, 386 | }; 387 | } 388 | // here it is declaration and initialisation, so i have to check the type of value on the other side 389 | // to know how to go about parsing 390 | if (this.expectTokenVal(tokens.assign)) { 391 | this.consume(); 392 | const leftTokenValues = this.tokenizer 393 | .getTokenLeftLine() 394 | .map((t) => t.value); 395 | switch (this.tokenizer.getCurrentToken().type) { 396 | case TokenType.Identifier: 397 | //todo 398 | if (leftTokenValues.length === 1) { 399 | // the identifier value 400 | let idtV = this.tokenizer.getCurrentToken().value; 401 | initializer = this.parseLiteral(); 402 | if (this.isDefinedVar(idtV)) { 403 | let idDt = ""; 404 | this.vars.map((v) => { 405 | if (v.val === idtV) { 406 | idDt = v.dataType; 407 | } 408 | }); 409 | dT = idDt; 410 | } 411 | let bL = this.nodes.length; 412 | this.vars.push({ dataType: dT, val: identifier, nodePos: bL }); 413 | } else { 414 | initializer = this.parseExpression(); 415 | } 416 | break; 417 | case TokenType.Literal: 418 | if (leftTokenValues.length === 1) { 419 | initializer = this.parseLiteral(); 420 | dT = "number"; 421 | let bL = this.nodes.length; 422 | this.vars.push({ dataType: dT, val: identifier, nodePos: bL }); 423 | } else { 424 | initializer = this.parseExpression(); 425 | } 426 | break; 427 | case TokenType.StringLiteral: 428 | //todo 429 | if ( 430 | leftTokenValues.length === 1 || 431 | this.expectPeek(TokenType.NewLine) 432 | ) { 433 | initializer = this.parseLiteral(); 434 | dT = "string"; 435 | let bL = this.nodes.length; 436 | this.vars.push({ dataType: dT, val: identifier, nodePos: bL }); 437 | } else { 438 | initializer = this.parseExpression(); 439 | } 440 | break; 441 | case TokenType.Punctuation: 442 | initializer = this.parseExpression(); 443 | break; 444 | case TokenType.Operator: 445 | //prefix operators 446 | if ( 447 | this.expectTokenVal(tokens.not) || 448 | this.expectTokenVal(tokens.sub) || 449 | this.expectTokenVal("--") || 450 | this.expectTokenVal("++") 451 | ) { 452 | //todo 453 | initializer = this.parseExpression(); 454 | break; 455 | } else { 456 | // another error, fallthrough 457 | } 458 | case TokenType.Keyword: 459 | if (this.expectTokenVal("true") || this.expectTokenVal("false")) { 460 | initializer = this.parseExpression(); 461 | this.vars.push({ 462 | dataType: "boolean", 463 | val: identifier, 464 | nodePos: this.nodes.length, 465 | }); 466 | } else { 467 | if (isAllowedKey(this.tokenizer.getCurrentToken().value)) { 468 | switch (this.tokenizer.getCurrentToken().value) { 469 | case "f": 470 | initializer = this.parseFunc(); 471 | break; 472 | default: 473 | initializer = this.parseLiteral(); 474 | } 475 | } 476 | } 477 | break; 478 | default: 479 | this.addError( 480 | `Unexpected token '${this.tokenizer.getCurrentToken()?.value}' of type ${this.tokenizer.getCurrentToken()?.type} at variable initialization for '${identifier}' - Expected a value (number, string, boolean, function, or expression)` 481 | ); 482 | this.tokenizer.toNewLine(); 483 | // an error (variable value can't be keyword or operator, but some things like () and [], {} may fall in punctuation which can be a variable) 484 | } 485 | return { 486 | type: ASTNodeType.VariableDeclaration, 487 | identifier, 488 | initializer, 489 | }; 490 | } else { 491 | this.addError( 492 | `Unexpected token '${this.tokenizer.getCurrentToken()?.value}' after variable identifier '${identifier}' - Expected '=' for variable assignment` 493 | ); 494 | this.consume(); 495 | this.tokenizer.toNewLine(); 496 | } 497 | } else { 498 | this.addError( 499 | `Unexpected token '${this.tokenizer.getCurrentToken()?.value}' of type ${this.tokenizer.getCurrentToken()?.type} in variable declaration - Expected identifier after 'l' keyword` 500 | ); 501 | this.consume(); 502 | this.tokenizer.toNewLine(); 503 | } 504 | } 505 | parseDefine() { 506 | if (this.expectPeek(TokenType.Identifier)) { 507 | this.consume(); 508 | let identifier = this.consume().value; 509 | let initializer; 510 | if (this.expectTokenVal("-")) { 511 | this.consume(); 512 | if (this.expectTokenVal(tokens.grT)) { 513 | this.consume(); 514 | initializer = this.parseLiteral(); 515 | this.defines.set(identifier, initializer.value); 516 | }else{ 517 | this.addError(`Unexpected token: ${this.consume().value}, expected >`) 518 | } 519 | }else{ 520 | this.addError(`Unexpected token: ${this.consume().value}, expected def chain ->`) 521 | } 522 | return { 523 | type: ASTNodeType.DefDecl, 524 | identifier, 525 | initializer, 526 | }; 527 | } else { 528 | this.addError( 529 | `Unexpected token type: '${this.tokenizer.peek().value}' (${this.tokenizer.peek().type}) after 'def' keyword - Expected identifier to define` 530 | ); 531 | this.tokenizer.toNewLine(); 532 | } 533 | } 534 | parseReturn() { 535 | if ( 536 | this.expectPeek(TokenType.NewLine) || 537 | this.expectPeekVal(";") || 538 | this.expectPeekVal("}") 539 | ) { 540 | let rtToken = this.consume(); 541 | return { 542 | type: ASTNodeType.Return, 543 | value: rtToken.value, 544 | }; 545 | } else { 546 | if ( 547 | this.expectPeek(TokenType.Identifier) || 548 | this.expectPeek(TokenType.Literal) || 549 | this.expectPeek(TokenType.StringLiteral) || 550 | isAllowedKey(this.tokenizer.peek().value) 551 | ) { 552 | this.consume(); 553 | const tk = this.parseExpression(); 554 | 555 | return { 556 | type: ASTNodeType.Return, 557 | initializer: tk, 558 | }; 559 | } 560 | this.addError(`Unexpected token '${this.tokenizer.getCurrentToken()?.value}' after return statement - Expected newline, semicolon, or end of block`); 561 | this.consume(); 562 | } 563 | } 564 | parseBreakNCont() { 565 | const keyword = this.consume(); // Get the break or continue keyword 566 | 567 | if ( 568 | this.expectToken(TokenType.NewLine) || 569 | this.expectToken(TokenType.EOF) || 570 | this.expectTokenVal(";") || 571 | this.expectTokenVal("}") 572 | ) { 573 | // Valid termination for break/continue 574 | if (this.expectToken(TokenType.NewLine) || this.expectTokenVal(";")) { 575 | this.consume(); 576 | } 577 | 578 | return { 579 | type: keyword.value === "break" ? ASTNodeType.Break : ASTNodeType.Continue, 580 | value: keyword.value, 581 | }; 582 | } else { 583 | this.addError( 584 | `Unexpected token '${this.tokenizer.getCurrentToken()?.value}' after ${keyword.value} keyword - Expected newline, semicolon, or end of block` 585 | ); 586 | this.tokenizer.toNewLine(); 587 | return { 588 | type: keyword.value === "break" ? ASTNodeType.Break : ASTNodeType.Continue, 589 | value: keyword.value, 590 | }; 591 | } 592 | } 593 | parseFunc() { 594 | this.consume(); 595 | let identifier; 596 | let params; 597 | let body; 598 | if (this.expectToken(TokenType.Identifier)) { 599 | // parse 600 | identifier = this.consume().value; 601 | if (this.expectTokenVal("(")) { 602 | params = this.parseFuncParams(); 603 | if (this.expectTokenVal("{")) { 604 | body = this.parseBlockStmt(); 605 | } else { 606 | this.addError(`Expected '{' for function body`); 607 | } 608 | return { 609 | type: ASTNodeType.FunctionDeclaration, 610 | identifier, 611 | params, 612 | body, 613 | }; 614 | } else { 615 | this.addError(`Expected '(' at function declaration`); 616 | return null; // Stop parsing this function 617 | } 618 | } else { 619 | // potential error, will assert at the end,if the function isn't called 620 | if (this.expectTokenVal("(")) { 621 | params = this.parseFuncParams(); 622 | if (this.expectTokenVal("{")) { 623 | body = this.parseBlockStmt(); 624 | } else { 625 | this.addError(`Expected '{' for function body`); 626 | } 627 | return { 628 | type: ASTNodeType.FunctionDeclaration, 629 | params, 630 | body, 631 | }; 632 | } else { 633 | this.addError(`Expected '(' at anonymouds or function declaration`); 634 | } 635 | } 636 | } 637 | parseFuncParams() { 638 | this.consume(); 639 | let params: ASTNode[] = []; 640 | while (!this.expectTokenVal(")")) { 641 | const arg = this.parseLiteral(); 642 | if (!arg) { 643 | this.addError(`SyntaxError: Invalid argument in function declaration - Expected parameter name`); 644 | break; // Prevent infinite loops if parseExpression fails 645 | } 646 | params.push(arg); // Collect the parsed argument 647 | if (this.expectTokenVal(",")) { 648 | this.consume(); // Consume the comma separator 649 | } else if (!this.expectTokenVal(")")) { 650 | this.addError(`SyntaxError: Expected ',' or ')' in function declaration parameters - Found '${this.tokenizer.getCurrentToken()?.value}' instead`); 651 | break; 652 | } 653 | } 654 | if (this.expectTokenVal(")")) { 655 | this.consume(); 656 | } else { 657 | this.addError(`SyntaxError: Unmatched parentheses in function declaration - Expected closing ')'`); 658 | } 659 | return params; 660 | } 661 | parseBlockStmt() { 662 | this.consume(); 663 | let body: ASTNode[] = []; 664 | while (!this.expectTokenVal("}")) { 665 | // Skip newlines and statement terminator 666 | if (this.expectToken(TokenType.NewLine) || this.expectTokenVal(";")) { 667 | this.consume(); 668 | continue; 669 | } 670 | let node = this.checkParseReturn(); 671 | body.push(node); 672 | } 673 | if (this.expectTokenVal("}")) { 674 | this.consume(); 675 | } else { 676 | this.addError(`Block not closed properly - Expected '}' to close block opened with '{'`); 677 | } 678 | return body; 679 | } 680 | parseIfElse() { 681 | this.consume(); 682 | let test; 683 | let consequence; 684 | let alternate; 685 | if (this.expectTokenVal("(")) { 686 | test = this.parseExpression(); 687 | } else { 688 | this.addError( 689 | `SyntaxError: Expected '(' for if condition, got '${this.tokenizer.getCurrentToken()?.value}' - If statements require parentheses around the condition` 690 | ); 691 | return null; 692 | } 693 | if (!this.expectTokenVal("{")) { 694 | this.addError( 695 | `SyntaxError: Expected '{' to start if statement body, got '${this.tokenizer.getCurrentToken()?.value}' - Code blocks must be wrapped in curly braces` 696 | ); 697 | return null; 698 | } 699 | consequence = this.parseBlockStmt(); 700 | if (!this.expectTokenVal("else")) { 701 | return { 702 | type: ASTNodeType.IfElse, 703 | test, 704 | consequence, 705 | }; 706 | } 707 | this.consume(); 708 | if (this.expectTokenVal("if") || this.expectTokenVal("{")) { 709 | if (this.expectTokenVal("{")) { 710 | alternate = this.parseBlockStmt(); 711 | } else { 712 | alternate = this.parseIfElse(); 713 | } 714 | return { 715 | type: ASTNodeType.IfElse, 716 | test, 717 | consequence, 718 | alternate, 719 | }; 720 | } else { 721 | this.addError( 722 | `SyntaxError: Unexpected token '${this.tokenizer.getCurrentToken()?.value}' after else keyword - Expected 'if' for else-if or '{' for else block` 723 | ); 724 | return null; 725 | } 726 | } 727 | parseWhileLoop() { 728 | this.consume(); 729 | let test; 730 | let body; 731 | if (!this.expectTokenVal("(")) { 732 | this.addError( 733 | `SyntaxError: Expected '(' for while condition, got '${this.tokenizer.getCurrentToken()?.value}' - While loops require parentheses around the condition` 734 | ); 735 | return null; 736 | } 737 | test = this.parseExpression(); 738 | if (!this.expectTokenVal("{")) { 739 | this.addError( 740 | `SyntaxError: Expected '{' to start while loop body, got '${this.tokenizer.getCurrentToken()?.value}' - Code blocks must be wrapped in curly braces` 741 | ); 742 | return null; 743 | } 744 | body = this.parseBlockStmt(); 745 | return { 746 | type: ASTNodeType.Loop, 747 | test, 748 | body, 749 | }; 750 | } 751 | parseForLoop(){ 752 | this.consume(); 753 | let initializer; 754 | let test; 755 | let upgrade; 756 | let body; 757 | if (!this.expectTokenVal("(")) { 758 | this.addError( 759 | `SyntaxError: Expected '(' for for loop, got '${this.tokenizer.getCurrentToken()?.value}' - For loops require parentheses around the initialization, condition, and update` 760 | ); 761 | return null; 762 | } 763 | this.consume(); 764 | initializer = this.parseVariable(); 765 | if (this.expectToken(TokenType.NewLine) || this.expectTokenVal(";")) { 766 | this.consume(); 767 | } 768 | test = this.parseExpression(); 769 | if (this.expectToken(TokenType.NewLine) || this.expectTokenVal(";")) { 770 | this.consume(); 771 | } 772 | upgrade = this.parseExpression(); 773 | this.consume() 774 | if (this.expectToken(TokenType.NewLine) || this.expectTokenVal(";")) { 775 | this.consume(); 776 | } 777 | if (!this.expectTokenVal("{")) { 778 | this.addError( 779 | `SyntaxError: Expected '{' to start for loop body, got '${this.tokenizer.getCurrentToken()?.value}' - Code blocks must be wrapped in curly braces` 780 | ); 781 | return null; 782 | } 783 | body = this.parseBlockStmt(); 784 | return{ 785 | type:ASTNodeType.Loop, 786 | initializer, 787 | test, 788 | upgrade, 789 | body 790 | } 791 | } 792 | checkParseReturn() { 793 | let baseToken = this.tokenizer.getCurrentToken(); 794 | let node; 795 | 796 | // First check if this is a keyword, then resolve any defines 797 | const resolvedValue = this.resolveDefine(baseToken.value); 798 | 799 | // Check if the resolved value is a keyword, even if the original wasn't 800 | const isResolvedKeyword = baseToken.type === TokenType.Keyword || 801 | (baseToken.type === TokenType.Identifier && this.defines.has(baseToken.value)); 802 | 803 | if (isResolvedKeyword) { 804 | switch (resolvedValue) { 805 | case tokens.l: 806 | node = this.parseVariable(); 807 | break; 808 | case "def": 809 | node = this.parseDefine(); 810 | break; 811 | case "return": 812 | node = this.parseReturn(); 813 | break; 814 | case "break": 815 | case "continue": 816 | node = this.parseBreakNCont(); 817 | break; 818 | case "f": 819 | node = this.parseFunc(); 820 | break; 821 | case "if": 822 | node = this.parseIfElse(); 823 | break; 824 | case "while": 825 | node = this.parseWhileLoop(); 826 | break; 827 | case "for": 828 | node = this.parseForLoop() 829 | break 830 | default: 831 | //hehe 832 | } 833 | } else { 834 | switch (baseToken.type) { 835 | case TokenType.Punctuation: 836 | if (baseToken.value === ";") { 837 | this.tokenizer.next(); 838 | } else { 839 | this.addError(`Unexpected punctuation: '${baseToken.value}' - Cannot start a statement with this punctuation`); 840 | this.tokenizer.next(); 841 | } 842 | break; 843 | case TokenType.NewLine: 844 | this.tokenizer.next(); 845 | break; 846 | case TokenType.Identifier: 847 | case TokenType.Literal: 848 | case TokenType.StringLiteral: 849 | node = this.parseExpression(); 850 | break; 851 | case TokenType.Operator: 852 | if ( 853 | this.expectTokenVal(tokens.not) || 854 | this.expectTokenVal(tokens.sub) || 855 | this.expectTokenVal("--") || 856 | this.expectTokenVal("++") 857 | ) { 858 | let nodeO = this.parseExpression(); 859 | nodeO && this.nodes.push(nodeO); 860 | } else { 861 | this.addError(`Unexpected operator: '${baseToken.value}' - Cannot start a statement with this operator (expected prefix operators like !, -, ++, --)`); 862 | this.tokenizer.next(); 863 | } 864 | break; 865 | default: 866 | this.addError(`Unexpected statement start: '${baseToken.value}' - Expected variable declaration (l), function (f), if statement, loop, or expression`); 867 | this.tokenizer.next(); 868 | //Syntax Error Likely 869 | } 870 | } 871 | return node; 872 | } 873 | checkAndParse() { 874 | let baseToken = this.tokenizer.getCurrentToken(); 875 | 876 | // First check if this is a keyword, then resolve any defines 877 | const resolvedValue = this.resolveDefine(baseToken.value); 878 | 879 | // Check if the resolved value is a keyword, even if the original wasn't 880 | const isResolvedKeyword = baseToken.type === TokenType.Keyword || 881 | (baseToken.type === TokenType.Identifier && this.defines.has(baseToken.value)); 882 | 883 | if (isResolvedKeyword) { 884 | switch (resolvedValue) { 885 | case tokens.l: 886 | let nodeV = this.parseVariable(); 887 | nodeV && this.nodes.push(nodeV); 888 | break; 889 | case "def": 890 | let nodeD = this.parseDefine(); 891 | nodeD && this.nodes.push(nodeD); 892 | break; 893 | case "return": 894 | let nodeR = this.parseReturn(); 895 | nodeR && this.nodes.push(nodeR); 896 | break; 897 | case "break": 898 | case "continue": 899 | let nodeBC = this.parseBreakNCont(); 900 | nodeBC && this.nodes.push(nodeBC); 901 | break; 902 | case "f": 903 | let nodeF = this.parseFunc(); 904 | nodeF && this.nodes.push(nodeF); 905 | break; 906 | case "if": 907 | let nodeIf = this.parseIfElse(); 908 | nodeIf && this.nodes.push(nodeIf); 909 | break; 910 | case "while": 911 | let nodeW = this.parseWhileLoop(); 912 | nodeW && this.nodes.push(nodeW); 913 | break; 914 | case "for": 915 | let nodeFo = this.parseForLoop(); 916 | nodeFo && this.nodes.push(nodeFo); 917 | break; 918 | default: 919 | this.consume(); 920 | //hehe 921 | } 922 | } else { 923 | switch (baseToken.type) { 924 | case TokenType.Punctuation: 925 | if (baseToken.value === ";") { 926 | this.tokenizer.next(); 927 | } else { 928 | this.addError(`Unexpected punctuation: '${baseToken.value}' - Cannot start a statement with this punctuation`); 929 | this.tokenizer.next(); 930 | } 931 | break; 932 | case TokenType.NewLine: 933 | this.tokenizer.next(); 934 | break; 935 | case TokenType.Identifier: 936 | case TokenType.Literal: 937 | case TokenType.StringLiteral: 938 | let nodeE = this.parseExpression(); 939 | nodeE && this.nodes.push(nodeE); 940 | break; 941 | case TokenType.Operator: 942 | if ( 943 | this.expectTokenVal(tokens.not) || 944 | this.expectTokenVal(tokens.sub) || 945 | this.expectTokenVal("--") || 946 | this.expectTokenVal("++") 947 | ) { 948 | let nodeO = this.parseExpression(); 949 | nodeO && this.nodes.push(nodeO); 950 | } else { 951 | this.addError(`Unexpected operator: '${baseToken.value}' - Cannot start a statement with this operator (expected prefix operators like !, -, ++, --)`); 952 | this.tokenizer.next(); 953 | } 954 | break; 955 | default: 956 | this.addError(`Unexpected statement start: '${baseToken.value}' - Expected variable declaration (l), function (f), if statement, loop, or expression`); 957 | this.tokenizer.next(); 958 | //Syntax Error Likely 959 | } 960 | } 961 | } 962 | start() { 963 | while (this.tokenizer.getCurrentToken().type !== TokenType.EOF) { 964 | this.checkAndParse(); 965 | } 966 | } 967 | } 968 | // Can now parse myprogram.ay, for now i'll build compiler for this and work on adding more language features after 969 | // Deno.readTextFileSync("./myprogram.ay") 970 | // let f = Bun.file("./myprogram.ay"); 971 | // const p = new Parser(await f.text()); 972 | // p.start(); 973 | // console.log(p.nodes, p.errors, p.vars); 974 | -------------------------------------------------------------------------------- /parser/tokens.ts: -------------------------------------------------------------------------------- 1 | export const tokens = { 2 | lParen: "(", 3 | rParen: ")", 4 | dot: ".", 5 | comma: ",", 6 | dot3: "...", 7 | colon: ":", 8 | semi: ";", 9 | lBrace: "{", 10 | rBrace: "}", 11 | lBrack: "[", 12 | rBrack: "]", 13 | assign: "=", 14 | add: "+", 15 | sub: "-", 16 | div: "/", 17 | mul: "*", 18 | rem: "%", 19 | shL: "<<", 20 | shR: ">>", 21 | grT: ">", 22 | lsT: "<", 23 | l: "l", 24 | or: "|", 25 | oror: "||", 26 | andand: "&&", 27 | not: "!", 28 | nullC: "??", 29 | equality: "==", 30 | inEqualty: "!=", 31 | subEql: "-=", 32 | addEql: "+=", 33 | mulEql: "*=", 34 | divEql: "/=", 35 | inc: "++", 36 | dec: "--", 37 | exp: "**", 38 | ororEql: "||=", 39 | andandEql: "&&=", 40 | grTEql: ">=", 41 | lsTEql: "<=", 42 | pow: "^", 43 | }; 44 | export const opToks = [ 45 | tokens.nullC, 46 | tokens.oror, 47 | tokens.andand, 48 | tokens.equality, 49 | tokens.inEqualty, 50 | tokens.lsT, 51 | tokens.grT, 52 | tokens.lsTEql, 53 | tokens.grTEql, 54 | tokens.add, 55 | tokens.sub, 56 | tokens.mul, 57 | tokens.div, 58 | tokens.pow, 59 | tokens.inc, 60 | tokens.dec, 61 | tokens.rem, 62 | tokens.shL, 63 | tokens.shR, 64 | ]; 65 | const keywords = [ 66 | "l", // custom: like 'let' 67 | "def", // custom 68 | "defer", 69 | "f", // custom: like 'function' 70 | "for", // custom & JavaScript 71 | "if", // custom & JavaScript 72 | "else", // custom & JavaScript 73 | "while", // custom & JavaScript 74 | "continue", // custom & JavaScript 75 | "return", // custom & JavaScript 76 | "break", // custom & JavaScript 77 | "do", // custom & JavaScript 78 | "imp@", 79 | "exp@", 80 | "from", 81 | "false", 82 | "true", 83 | // JavaScript keywords (non-function/variable declaration) 84 | "class", 85 | "const", 86 | "debugger", 87 | "delete", 88 | "extends", 89 | "finally", 90 | "in", 91 | "instanceof", 92 | "new", 93 | "null", 94 | "super", 95 | "switch", 96 | "this", 97 | "throw", 98 | "try", 99 | "typeof", 100 | "void", 101 | "with", 102 | "yield", 103 | ]; 104 | const allowedKeysAsVal = ["true", "false", "this", "f"/* for functions as variables */, "new"] 105 | export function isAllowedKey(key:string){ 106 | return allowedKeysAsVal.includes(key) 107 | } 108 | export enum TokenType { 109 | Identifier, 110 | Operator, 111 | Keyword, 112 | Literal, 113 | StringLiteral, 114 | Whitespace, 115 | Punctuation, 116 | SingleLineComment, 117 | MultiLineComment, 118 | NewLine, 119 | EOF, 120 | Unknown, 121 | } 122 | 123 | export interface Token { 124 | type: TokenType; 125 | value: string; 126 | line: number; 127 | column: number; 128 | } 129 | 130 | export function isKeyword(value: string) { 131 | return keywords.includes(value); 132 | } 133 | function tokenize(line: string) { 134 | // keep track of each token in an array 135 | const tokens: Token[] = []; 136 | // holds current token and current token type values 137 | let currentToken = ""; 138 | let currentType: TokenType = TokenType.Identifier; 139 | // keeps track of string quotes and whether we are in a string or not(sOpen) 140 | let qChar = ""; 141 | let sOpen = false; 142 | //loop through each and every character to scan them depending on how they are 143 | for (let i = 0; i < line.length; i++) { 144 | const nextChar = line[i + 1] || ""; 145 | // multi line comments 146 | if (line[i] === "/" && nextChar === "*"){ 147 | currentType = TokenType.MultiLineComment; 148 | currentToken = "/*" 149 | i++ // skip * in /* 150 | continue 151 | } 152 | if (line[i] === "*" && nextChar === "/"){ 153 | currentToken += "*/" 154 | i++ // skip / in */ 155 | tokens.push({type: currentType, value:currentToken, line: 1, column: 1}) 156 | currentType = TokenType.Identifier; 157 | currentToken = "" 158 | continue 159 | } 160 | 161 | 162 | // Detect Windows-style newline \r\n 163 | if ((line[i] === "\r" && nextChar === "\n") && currentType !== TokenType.MultiLineComment) { 164 | if (currentType === TokenType.SingleLineComment){ 165 | tokens.push({type: currentType, value:currentToken, line: 1, column: 1}) 166 | } 167 | currentType = TokenType.NewLine 168 | tokens.push({ type: currentType, value: "\r\n", line: 1, column: 1 }); 169 | i++; // Skip the \n part of \r\n 170 | currentToken = ""; // Clear currentToken if needed for new lines 171 | continue; 172 | } 173 | // Detect Mac-style \r and Unix-style \n newlines individually 174 | else if ((line[i] === "\r" || line[i] === "\n") && currentType !== TokenType.MultiLineComment) { 175 | if (currentType === TokenType.SingleLineComment){ 176 | tokens.push({type: currentType, value:currentToken, line: 1, column: 1}) 177 | } 178 | currentType = TokenType.NewLine 179 | tokens.push({ type: currentType, value: line[i], line: 1, column: 1 }); 180 | currentToken = ""; 181 | continue; 182 | } 183 | 184 | // this checks if it's a string quote character, controls the value of sOpen 185 | // notice how we also make sure we are not in a comment by checking the type 186 | if ( 187 | (line[i] === '"' || line[i] === "'") && 188 | currentType !== TokenType.SingleLineComment && 189 | currentType !== TokenType.MultiLineComment 190 | ) { 191 | qChar = line[i]; 192 | // checks if the string was already open so we know that's a closing quote so 193 | // we can make sOpen false and clear currentToken, and push the entire string into the tokens array 194 | if (sOpen) { 195 | // extra validation to make sure it's the proper end to the star 196 | if (currentToken[0] === qChar) { 197 | currentToken += qChar; 198 | tokens.push({ type: currentType, value: currentToken, line: 1, column: 1 }); 199 | //cleanup 200 | currentToken = ""; 201 | sOpen = false; 202 | } 203 | // else we know that this is just the opening of a string, so we set the currentType 204 | // and we make sOpen true 205 | } else { 206 | currentType = TokenType.StringLiteral; 207 | sOpen = true; 208 | } 209 | } 210 | // keep adding every character as a comment, but since we only expect a line this is fine as it continues to the end of the line 211 | //keep adding string characters until sOpen is false, i.e it's closed with the ending quotechar 212 | if (sOpen || currentType === TokenType.SingleLineComment || currentType === TokenType.MultiLineComment) { 213 | currentToken += line[i]; 214 | //start other tests, first for identifiers and keywords, notice you'd always see... 215 | // !sOpen && currentType !== TokenType.SingleLineComment, to make sure the code in the 216 | // block doesn't run when a string is open or when a comment is on 217 | } 218 | if ( 219 | /[a-zA-Z_@]/.test(line[i]) && 220 | !sOpen && 221 | currentType !== TokenType.SingleLineComment && 222 | currentType !== TokenType.MultiLineComment 223 | ) { 224 | //previous token is an identifier, just add up the character to it 225 | if (currentType == TokenType.Identifier) { 226 | currentToken += line[i]; 227 | // it was another token type, now an identifier so we initialise the type and value fresh 228 | } else { 229 | currentType = TokenType.Identifier; 230 | currentToken = line[i]; 231 | } //checks if it's the last character or not, passes if not last char 232 | if (line.length - 1 >= i + 1) { 233 | if (!/[a-zA-Z_@0-9]/.test(line[i + 1])) { 234 | //if it's not the last character and the next character fails the test then we can push it 235 | if (isKeyword(currentToken)) { 236 | // checks keyword or identifier 237 | tokens.push({ type: TokenType.Keyword, value: currentToken, line: 1, column: 1 }); 238 | currentToken = ""; 239 | } else { 240 | tokens.push({ type: currentType, value: currentToken, line: 1, column: 1 }); 241 | currentToken = ""; 242 | } 243 | } 244 | } 245 | } else if ( 246 | /\s/.test(line[i]) && 247 | !sOpen && 248 | currentType !== TokenType.SingleLineComment && 249 | currentType !== TokenType.MultiLineComment 250 | ) { 251 | currentType = TokenType.Whitespace; 252 | if (currentToken.length > 0 && /\s/.test(currentToken)) { 253 | currentToken += line[i]; 254 | } else { 255 | currentToken = line[i]; 256 | } 257 | if (line.length - 1 >= i + 1) { 258 | if (!/\s/.test(line[i + 1])) { 259 | tokens.push({ type: currentType, value: currentToken, line: 1, column: 1 }); 260 | currentToken = ""; 261 | } 262 | } 263 | } else if ( 264 | /[+*/%=<>&|!?^-]/.test(line[i]) && 265 | !sOpen && 266 | currentType !== TokenType.SingleLineComment && 267 | currentType !== TokenType.MultiLineComment 268 | ) { 269 | currentType = TokenType.Operator; 270 | if (currentToken.length > 0 && /[+*/%=<>&|!?-]/.test(currentToken)) { 271 | switch (currentToken.length) { 272 | case 1: 273 | if (currentToken === "/" && line[i] === "/") { 274 | // This is a single line comment // 275 | currentType = TokenType.SingleLineComment; 276 | currentToken += line[i]; 277 | } else if (currentToken !== "^") { 278 | if (currentToken === line[i]) { 279 | currentToken += line[i]; 280 | } else if (line[i] === "=") { 281 | currentToken += line[i]; 282 | } else { 283 | tokens.push({ type: currentType, value: currentToken, line: 1, column: 1 }); 284 | currentToken = line[i]; 285 | } 286 | } else { 287 | tokens.push({ type: currentType, value: currentToken, line: 1, column: 1 }); 288 | currentToken = line[i]; 289 | } 290 | break; 291 | case 2: 292 | if ( 293 | (currentToken === ">>" || currentToken === "<<") && 294 | (line[i] === ">" || line[i] === "<") 295 | ) { 296 | currentToken += line[i]; 297 | } 298 | break; 299 | default: 300 | currentType = TokenType.Unknown; 301 | currentToken += line[i]; 302 | } 303 | } else { 304 | currentToken = line[i]; 305 | } 306 | if (line.length - 1 >= i + 1) { 307 | if ( 308 | !/[+*/%=<>&|!?-]/.test(line[i + 1]) && 309 | currentType !== TokenType.SingleLineComment 310 | ) { 311 | tokens.push({ type: currentType, value: currentToken, line: 1, column: 1 }); 312 | currentToken = ""; 313 | } 314 | } 315 | // hmm: /^-?\d+(_?\d+)*(?:\.\d+)?$/ 316 | } else if ( 317 | /\d/.test(line[i]) && 318 | !sOpen && 319 | currentType !== TokenType.SingleLineComment && 320 | currentType !== TokenType.MultiLineComment 321 | ) { 322 | // If we're already building an identifier, add the digit to it 323 | if (currentType === TokenType.Identifier) { 324 | currentToken += line[i]; 325 | // Check if next character would end the identifier 326 | if (line.length - 1 >= i + 1) { 327 | if (!/[a-zA-Z_@0-9]/.test(line[i + 1])) { 328 | tokens.push({ type: currentType, value: currentToken, line: 1, column: 1 }); 329 | currentToken = ""; 330 | } 331 | } 332 | } else { 333 | // Start a new literal (number) 334 | currentType = TokenType.Literal; 335 | if ( 336 | currentToken.length > 0 && 337 | (/\d/.test(currentToken) || currentToken.endsWith(".")) 338 | ) { 339 | if (/\d/.test(currentToken) || currentToken.endsWith(".")) { 340 | currentToken += line[i]; 341 | } 342 | } else { 343 | currentToken = line[i]; 344 | } 345 | if (line.length - 1 >= i + 1) { 346 | if (!/\d/.test(line[i + 1]) && line[i + 1] !== ".") { 347 | tokens.push({ type: currentType, value: currentToken, line: 1, column: 1 }); 348 | currentToken = ""; 349 | } 350 | } 351 | } 352 | } else if ( 353 | /[(){}[\]:;,.]/.test(line[i]) && 354 | !sOpen && 355 | currentType !== TokenType.SingleLineComment && 356 | currentType !== TokenType.MultiLineComment 357 | ) { 358 | if ( 359 | currentType === TokenType.Literal && 360 | line[i] === "." && 361 | !currentToken.includes(".") && 362 | currentToken.length > 0 363 | ) { 364 | currentToken += line[i]; 365 | } else { 366 | currentType = TokenType.Punctuation; 367 | currentToken = line[i]; 368 | tokens.push({ type: currentType, value: currentToken, line: 1, column: 1 }); 369 | currentToken = ""; 370 | } 371 | } 372 | } 373 | if (currentToken !== "") { 374 | if (currentType === TokenType.Identifier && isKeyword(currentToken)){ 375 | currentType = TokenType.Keyword 376 | } 377 | tokens.push({ type: currentType, value: currentToken, line: 1, column: 1 }); 378 | } 379 | tokens.push({type: TokenType.EOF, value:"", line: 1, column: 1}) 380 | 381 | // Fix line and column tracking for all tokens 382 | let lineNo = 1; 383 | let colNo = 1; 384 | tokens.forEach((t) => { 385 | t.line = lineNo; 386 | t.column = colNo; 387 | 388 | // Handle multi-line comments and strings that contain newlines 389 | if (t.value.includes("\n") || t.value.includes("\r\n")) { 390 | const lines = t.value.split(/\r\n|\r|\n/); 391 | lineNo += lines.length - 1; 392 | if (lines.length > 1) { 393 | colNo = lines[lines.length - 1].length + 1; 394 | } else { 395 | colNo += t.value.length; 396 | } 397 | } else if (t.type === TokenType.NewLine) { 398 | lineNo++; 399 | colNo = 1; 400 | } else { 401 | colNo += t.value.length; 402 | } 403 | }); 404 | //ignore whitespace and comment tokens 405 | return tokens.filter((t) => t.type !== TokenType.Whitespace && t.type !== TokenType.SingleLineComment && t.type !== TokenType.MultiLineComment); 406 | } 407 | 408 | /** 409 | * class TokenGen: 410 | * provides methods and functionalities for using and moving through tokens 411 | */ 412 | export class TokenGen { 413 | tokenizeLine: (line: string) => Token[]; 414 | lines: string[]; 415 | currentLine: number; 416 | tokens:Token[]; 417 | currentTokenNo: number; 418 | constructor(file: string) { 419 | this.tokenizeLine = tokenize; 420 | this.lines = file.includes("\r\n") ? file.split("\r\n") : file.split("\n"); 421 | this.tokens = this.tokenizeWithLineNumbers(file); 422 | this.currentLine = 0; 423 | this.currentTokenNo = 0; 424 | } 425 | 426 | private tokenizeWithLineNumbers(file: string): Token[] { 427 | // Tokenize the entire file at once 428 | const tokens = this.tokenizeLine(file); 429 | return tokens; 430 | } 431 | /** 432 | * next() => token moves to the next non comment token 433 | */ 434 | next(): void { 435 | if (this.tokens[this.currentTokenNo].type !== TokenType.EOF){ 436 | this.currentTokenNo++; 437 | while(this.tokens[this.currentTokenNo].type === TokenType.SingleLineComment ||this.tokens[this.currentTokenNo].type === TokenType.MultiLineComment){ 438 | this.currentTokenNo++; 439 | } 440 | } 441 | } 442 | /** 443 | * back() => token moves back to the next non comment token 444 | */ 445 | back() { 446 | if (this.currentTokenNo !== 0) { 447 | this.currentTokenNo--; 448 | while(this.tokens[this.currentTokenNo].type === TokenType.SingleLineComment ||this.tokens[this.currentTokenNo].type === TokenType.MultiLineComment){ 449 | this.currentTokenNo--; 450 | } 451 | } 452 | } 453 | /** 454 | * Checks and returns future non comment tokens, Does not change the current token 455 | * @param steps: Number of steps to peek or look ahead. 456 | * @returns the token peeked, if no steps is provided returns the next token 457 | */ 458 | peek(steps?: number): Token { 459 | if(steps && (steps + 1 + this.currentTokenNo) < this.tokens.length){ 460 | let pkNo = steps + 1 + this.currentTokenNo 461 | if(this.tokens[pkNo]){ 462 | return this.tokens[pkNo] 463 | }else{ 464 | return this.getCurrentToken() 465 | } 466 | }else{ 467 | let pkNo = this.currentTokenNo + 1 468 | if(this.tokens[pkNo]){ 469 | 470 | return this.tokens[pkNo] 471 | }else{ 472 | return this.getCurrentToken() 473 | } 474 | 475 | } 476 | 477 | } 478 | /** 479 | * Like next(), but optionally takes a number (steps) to move through the next token 480 | * @param steps Number of tokens to skip 481 | * @returns The new current token after skipping 482 | */ 483 | skip(steps?: number) { 484 | if(steps && (steps + this.currentTokenNo) < this.tokens.length){ 485 | this.currentTokenNo += steps 486 | if (this.tokens[this.currentTokenNo]){ 487 | return this.tokens[this.currentTokenNo] 488 | }else{ 489 | return this.getCurrentToken() 490 | } 491 | }else{ 492 | this.next() 493 | return this.getCurrentToken() 494 | } 495 | } 496 | /** 497 | * 498 | * @returns The current token 499 | */ 500 | getCurrentToken() { 501 | return this.tokens[this.currentTokenNo] 502 | } 503 | 504 | /** 505 | * 506 | * @returns The current line number (1-based) 507 | */ 508 | getCurrentLineNumber() { 509 | return this.tokens[this.currentTokenNo]?.line || 1; 510 | } 511 | 512 | /** 513 | * 514 | * @returns The current column number (1-based) 515 | */ 516 | getCurrentColumnNumber() { 517 | return this.tokens[this.currentTokenNo]?.column || 1; 518 | } 519 | /** 520 | * 521 | * @returns An array of all the tokens left 522 | */ 523 | getRemainingToken() { 524 | let tokensLeft: Token[] = this.tokens.slice(this.currentTokenNo + 1); 525 | return tokensLeft 526 | } 527 | /** 528 | * 529 | * @returns an array of all the tokens left in a line 530 | */ 531 | getTokenLeftLine() { 532 | let leftTokens = this.getRemainingToken(); 533 | let leftLineToken:Token[] = [] 534 | for (let i = 0; i < leftTokens.length; i++){ 535 | if(leftTokens[i].type === TokenType.NewLine){ 536 | leftLineToken.push(leftTokens[i]); 537 | break; 538 | } 539 | leftLineToken.push(leftTokens[i]); 540 | } 541 | if (this.getCurrentToken().type === TokenType.NewLine){ 542 | leftLineToken = [this.getCurrentToken()] 543 | } 544 | return leftLineToken 545 | } 546 | /** 547 | * 548 | * @returns An array of all the tokens in the current line 549 | */ 550 | getFullLineToken() { 551 | let flToken:Token[] = []; 552 | for (let i = this.currentTokenNo; this.tokens[i].type != TokenType.NewLine; i--){ 553 | flToken.push(this.tokens[i]) 554 | } 555 | flToken.reverse() 556 | flToken.push(...this.getTokenLeftLine()) 557 | return flToken; 558 | } 559 | /** 560 | * Moves the token ahead to a new line 561 | */ 562 | toNewLine(){ 563 | const leftTokens = this.getRemainingToken() 564 | for (let i = 0; i < leftTokens.length; i++){ 565 | this.next() 566 | if(leftTokens[i].type === TokenType.NewLine){ 567 | this.next() 568 | break 569 | } 570 | } 571 | } 572 | } 573 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "lib": ["ES2020"], 6 | "outDir": "./dist", 7 | "rootDir": "./", 8 | "strict": false, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "resolveJsonModule": true, 13 | "declaration": false, 14 | "declarationMap": false, 15 | "sourceMap": true, 16 | "removeComments": false, 17 | "noImplicitAny": false, 18 | "noImplicitReturns": false, 19 | "noUnusedLocals": false, 20 | "noUnusedParameters": false, 21 | "exactOptionalPropertyTypes": false, 22 | "allowUnreachableCode": true, 23 | "allowUnusedLabels": true 24 | }, 25 | "include": [ 26 | "**/*.ts", 27 | "**/*.js" 28 | ], 29 | "exclude": [ 30 | "node_modules", 31 | "dist", 32 | "**/*.test.ts" 33 | ], 34 | "preserveShebangs": true 35 | } --------------------------------------------------------------------------------