enumgen.js (11232B)
1 "use strict"; 2 3 const fs = require("fs"); 4 const path = require("path"); 5 6 // ============================================================================ 7 // [Tokenizer] 8 // ============================================================================ 9 10 // The list of "token types" which our lexer understands: 11 const tokenizerPatterns = [ 12 { type: "space" , re: /^\s+/ }, 13 { type: "comment" , re: /^(\/\/.*(\n|$)|\/\*.*\*\/)/ }, 14 { type: "symbol" , re: /^[a-zA-Z_]\w*/ }, 15 { type: "integer" , re: /^(-?\d+|0[x|X][0-9A-Fa-f]+)(l)?(l)?(u)?\b/ }, 16 { type: "comma" , re: /^,/ }, 17 { type: "operator", re: /(\+|\+\+|-|--|\/|\*|<<|>>|=|==|<|<=|>|>=|&|&&|\||\|\||\^|~|!)/ }, 18 { type: "paren" , re: /^[\(\)\{\}\[\]]/ } 19 ]; 20 21 function nextToken(input, from, patterns) { 22 if (from >= input.length) { 23 return { 24 type: "end", 25 begin: from, 26 end: from, 27 content: "" 28 } 29 } 30 31 const s = input.slice(from); 32 for (var i = 0; i < patterns.length; i++) { 33 const pattern = patterns[i]; 34 const result = s.match(pattern.re); 35 36 if (result !== null) { 37 const content = result[0]; 38 return { 39 type: pattern.type, 40 begin: from, 41 end: from + content.length, 42 content: content 43 }; 44 } 45 } 46 47 return { 48 type: "invalid", 49 begin: from, 50 end: from + 1, 51 content: input[from] 52 }; 53 } 54 55 class Tokenizer { 56 constructor(input, patterns) { 57 this.input = input; 58 this.index = 0; 59 this.patterns = patterns; 60 } 61 62 next() { 63 for (;;) { 64 const token = nextToken(this.input, this.index, this.patterns); 65 this.index = token.end; 66 if (token.type === "space" || token.type === "comment") 67 continue; 68 return token; 69 } 70 } 71 72 revert(token) { 73 this.index = token.begin; 74 } 75 } 76 77 // ============================================================================ 78 // [Parser] 79 // ============================================================================ 80 81 function parseEnum(input) { 82 const map = Object.create(null); 83 const tokenizer = new Tokenizer(input, tokenizerPatterns); 84 85 var value = -1; 86 87 for (;;) { 88 var token = tokenizer.next(); 89 if (token.type === "end") 90 break; 91 92 if (token.type === "symbol") { 93 const symbol = token.content; 94 token = tokenizer.next(); 95 if (token.content === "=") { 96 token = tokenizer.next(); 97 if (token.type !== "integer") 98 throw Error(`Expected an integer after symbol '${symbol} = '`); 99 value = parseInt(token.content); 100 } 101 else { 102 value++; 103 } 104 105 if (!Object.hasOwn(map, symbol)) 106 map[symbol] = value; 107 else 108 console.log(`${symbol} already defined, skipping...`); 109 110 token = tokenizer.next(); 111 if (token.type !== "comma") 112 tokenizer.revert(token); 113 continue; 114 } 115 116 throw Error(`Unexpected token ${token.type} (${token.content})`); 117 } 118 119 return map; 120 } 121 122 // ============================================================================ 123 // [Stringify] 124 // ============================================================================ 125 126 function compare(a, b) { 127 return a < b ? -1 : a == b ? 0 : 1; 128 } 129 130 function compactedSize(table) { 131 var size = 0; 132 for (var i = 0; i < table.length; i++) 133 size += table[i].name.length + 1; 134 return size; 135 } 136 137 function indexTypeFromSize(size) { 138 if (size <= 256) 139 return 'uint8_t'; 140 else if (size <= 65536) 141 return 'uint16_t'; 142 else 143 return 'uint32_t'; 144 } 145 146 function indent(s, indentation) { 147 var lines = s.split(/\r?\n/g); 148 if (indentation) { 149 for (var i = 0; i < lines.length; i++) { 150 var line = lines[i]; 151 if (line) lines[i] = indentation + line; 152 } 153 } 154 155 return lines.join("\n"); 156 } 157 158 function stringifyEnum(map, options) { 159 var output = ""; 160 161 const stripPrefix = options.strip; 162 const outputPrefix = options.output; 163 164 var max = -1; 165 var table = []; 166 167 for (var k in map) { 168 var name = k; 169 if (stripPrefix) { 170 if (name.startsWith(stripPrefix)) 171 name = name.substring(stripPrefix.length); 172 else 173 throw Error(`Cannot strip prefix '${stripPrefix}' in '${k}'`); 174 } 175 176 table.push({ name: name, value: map[k] }); 177 max = Math.max(max, map[k]); 178 } 179 180 table.sort(function(a, b) { return compare(a.value, b.value); }); 181 182 const unknownIndex = compactedSize(table); 183 table.push({ name: "<Unknown>", value: max + 1 }); 184 185 const indexType = indexTypeFromSize(compactedSize(table)); 186 187 function buildStringData() { 188 var s = ""; 189 for (var i = 0; i < table.length; i++) { 190 s += ` "${table[i].name}\\0"`; 191 if (i == table.length - 1) 192 s += `;`; 193 s += `\n`; 194 } 195 return s; 196 } 197 198 function buildIndexData() { 199 var index = 0; 200 var indexArray = []; 201 202 for (var i = 0; i < table.length; i++) { 203 while (indexArray.length < table[i].value) 204 indexArray.push(unknownIndex); 205 206 indexArray.push(index); 207 index += table[i].name.length + 1; 208 } 209 210 var s = ""; 211 var line = ""; 212 var pos = 0; 213 214 for (var i = 0; i < indexArray.length; i++) { 215 if (line) 216 line += " "; 217 218 line += `${indexArray[i]}`; 219 if (i != indexArray.length - 1) 220 line += `,`; 221 222 if (i == indexArray.length - 1 || line.length >= 72) { 223 s += ` ${line}\n`; 224 line = ""; 225 } 226 } 227 228 return s; 229 } 230 231 output += `static const char ${outputPrefix}_data[] =\n` + buildStringData() + `\n`; 232 output += `static const ${indexType} ${outputPrefix}_index[] = {\n` + buildIndexData() + `};\n`; 233 234 return output; 235 } 236 237 // ============================================================================ 238 // [FileSystem] 239 // ============================================================================ 240 241 function walkDir(baseDir) { 242 function walk(baseDir, nestedPath, out) { 243 fs.readdirSync(baseDir).forEach((file) => { 244 const stat = fs.statSync(path.join(baseDir, file)); 245 if (stat.isDirectory()) { 246 if (!stat.isSymbolicLink()) 247 walk(path.join(baseDir, file), path.join(nestedPath, file), out) 248 } 249 else { 250 out.push(path.join(nestedPath, file)); 251 } 252 }); 253 return out; 254 } 255 256 return walk(baseDir, "", []); 257 } 258 259 // ============================================================================ 260 // [Generator] 261 // ============================================================================ 262 263 class Generator { 264 constructor(options) { 265 this.enumMap = Object.create(null); 266 this.outputs = []; 267 268 this.verify = options.verify; 269 this.baseDir = options.baseDir; 270 this.noBackup = options.noBackup; 271 } 272 273 readEnums() { 274 console.log(`Scanning: ${this.baseDir}`); 275 walkDir(this.baseDir).forEach((fileName) => { 276 if (/\.(cc|cpp|h|hpp)$/.test(fileName)) { 277 const content = fs.readFileSync(path.join(this.baseDir, fileName), "utf8"); 278 this.addEnumsFromSource(fileName, content); 279 280 if (/@EnumStringBegin(\{.*\})@/.test(content)) 281 this.outputs.push(fileName); 282 } 283 }); 284 } 285 286 writeEnums() { 287 this.outputs.forEach((fileName) => { 288 console.log(`Output: ${fileName}`); 289 290 const oldContent = fs.readFileSync(path.join(this.baseDir, fileName), "utf8"); 291 const newContent = this.injectEnumsToSource(oldContent); 292 293 if (oldContent != newContent) { 294 if (this.verify) { 295 console.log(` FAILED: File is not up to date.`); 296 process.exit(1); 297 } 298 else { 299 if (!this.noBackup) { 300 fs.writeFileSync(path.join(this.baseDir, fileName + ".backup"), oldContent, "utf8"); 301 console.log(` Created ${fileName}.backup`); 302 } 303 fs.writeFileSync(path.join(this.baseDir, fileName), newContent, "utf8"); 304 console.log(` Updated ${fileName}`); 305 } 306 } 307 else { 308 console.log(` File is up to date.`); 309 } 310 }); 311 } 312 313 addEnumsFromSource(fileName, src) { 314 var found = false; 315 const matches = [...src.matchAll(/(?:@EnumValuesBegin(\{.*\})@|@EnumValuesEnd@)/g)]; 316 317 for (var i = 0; i < matches.length; i += 2) { 318 const def = matches[i]; 319 const end = matches[i + 1]; 320 321 if (!def[0].startsWith("@EnumValuesBegin")) 322 throw new Error(`Cannot start with '${def[0]}'`); 323 324 if (!end) 325 throw new Error(`Missing @EnumValuesEnd for '${def[0]}'`); 326 327 if (!end[0].startsWith("@EnumValuesEnd@")) 328 throw new Error(`Expected @EnumValuesEnd@ for '${def[0]}' and not '${end[0]}'`); 329 330 const options = JSON.parse(def[1]); 331 const enumName = options.enum; 332 333 if (!enumName) 334 throw Error(`Missing 'enum' in '${def[0]}`); 335 336 if (Object.hasOwn(this.enumMap, enumName)) 337 throw new Error(`Enumeration '${enumName}' is already defined`); 338 339 const startIndex = src.lastIndexOf("\n", def.index) + 1; 340 const endIndex = end.index + end[0].length; 341 342 if (startIndex === -1 || startIndex > endIndex) 343 throw new Error(`Internal Error, indexes have unexpected values: startIndex=${startIndex} endIndex=${endIndex}`); 344 345 if (!found) { 346 found = true; 347 console.log(`Found: ${fileName}`); 348 } 349 350 console.log(` Parsing Enum: ${enumName}`); 351 this.enumMap[enumName] = parseEnum(src.substring(startIndex, endIndex)); 352 } 353 } 354 355 injectEnumsToSource(src) { 356 const matches = [...src.matchAll(/(?:@EnumStringBegin(\{.*\})@|@EnumStringEnd@)/g)]; 357 var delta = 0; 358 359 for (var i = 0; i < matches.length; i += 2) { 360 const def = matches[i]; 361 const end = matches[i + 1]; 362 363 if (!def[0].startsWith("@EnumStringBegin")) 364 throw new Error(`Cannot start with '${def[0]}'`); 365 366 if (!end) 367 throw new Error(`Missing @EnumStringEnd@ for '${def[0]}'`); 368 369 if (!end[0].startsWith("@EnumStringEnd@")) 370 throw new Error(`Expected @EnumStringEnd@ for '${def[0]}' and not '${end[0]}'`); 371 372 const options = JSON.parse(def[1]); 373 const enumName = options.enum; 374 375 if (!enumName) 376 throwError(`Missing 'name' in '${def[0]}`); 377 378 if (!Object.hasOwn(this.enumMap, enumName)) 379 throw new Error(`Enumeration '${enumName}' not found`); 380 381 console.log(` Injecting Enum: ${enumName}`); 382 383 const startIndex = src.indexOf("\n", def.index + delta) + 1; 384 const endIndex = src.lastIndexOf("\n", end.index + delta) + 1; 385 386 if (startIndex === -1 || endIndex === -1 || startIndex > endIndex) 387 throw new Error(`Internal Error, indexes have unexpected values: startIndex=${startIndex} endIndex=${endIndex}`); 388 389 // Calculate the indentation. 390 const indentation = (function() { 391 const begin = src.lastIndexOf("\n", def.index + delta) + 1; 392 const end = src.indexOf("/", begin); 393 return src.substring(begin, end); 394 })(); 395 396 const newContent = indent(stringifyEnum(this.enumMap[enumName], options), indentation); 397 src = src.substring(0, startIndex) + newContent + src.substring(endIndex); 398 399 delta -= endIndex - startIndex; 400 delta += newContent.length; 401 } 402 403 return src; 404 } 405 } 406 407 const generator = new Generator({ 408 baseDir : path.resolve(__dirname, "../src"), 409 verify : process.argv.indexOf("--verify") !== -1, 410 noBackup: process.argv.indexOf("--no-backup") !== -1 411 }); 412 413 generator.readEnums(); 414 generator.writeEnums();