UNPKG

@syntaxs/compiler

Version:

Compiler used to compile Syntax Script projects.

315 lines (314 loc) 14.1 kB
import { CompilerError, NodeType, TokenType, statementIsA } from './types.js'; import { dirname, join } from 'path'; import { existsSync, readFileSync, readdirSync, statSync, writeFileSync } from 'fs'; import { sysparser, syxparser } from './ast.js'; import { tokenizeSys, tokenizeSyx } from './lexer.js'; /** * Main class used to compile a folder containing syntax script declaration (.syx) and syntax script (.sys) files. * @version 1.0.1 * @since 0.0.1-alpha */ export class SyntaxScriptCompiler { rootDir; outDir; mainFileFormat; exportData = {}; /** * Constructs a new compiler. * @param {string} rootDir Root dir to search for source files. * @param {string} outDir Out dir to write compiled files. * @param {string} format File format to compile. * @author efekos * @version 1.0.1 * @since 0.0.1-alpha */ constructor(rootDir, outDir, format) { this.rootDir = join(process.cwd(), rootDir); this.outDir = join(process.cwd(), outDir); this.mainFileFormat = format; } /** * Parses .syx files and compiles .sys files using them. * @author efekos * @since 0.0.1-alpha * @version 1.0.0 */ async compile() { await this.compileSyxFiles(this.rootDir); await this.compileSysFiles(this.rootDir); return Promise.resolve(); } /** * Compiles every .syx file found in the path. * @param {string} folderPath A folder path to search for .syx files. * @author efekos * @version 1.0.0 * @since 0.0.1-alpha */ compileSyxFiles(folderPath) { const files = readdirSync(folderPath); files.forEach(f => { if (f.endsWith('.syx')) this.compileSyx(join(folderPath, f)); else if (statSync(join(folderPath, f)).isDirectory()) this.compileSyxFiles(join(folderPath, f)); }); } /** * Compiles one .syx file from the path given. * @param {string} file Path to a file to compile. * @author efekos * @version 1.0.6 * @since 0.0.2-alpha */ compileSyx(file) { const ast = syxparser.parseTokens(tokenizeSyx(readFileSync(file).toString()), file); const out = []; ast.body.forEach(statement => { if (!statement.modifiers.some(token => token.type === TokenType.ExportKeyword)) return; if (statementIsA(statement, NodeType.Operator)) { //# Generate regexMatcher const regexMatcher = CompilerFunctions.generateRegexMatcher(statement); const operatorStmtExport = { imports: {}, outputGenerators: {}, regexMatcher, type: ExportType.Operator }; //# Handle statements statement.body.forEach(stmt => { if (stmt.type === NodeType.Compile) { const compileStmt = stmt; compileStmt.formats.forEach(frmt => { if (operatorStmtExport.outputGenerators[frmt.value] !== undefined) throw new CompilerError(compileStmt.range, `Duplicate file format at compile statement \'${frmt}\'`); operatorStmtExport.outputGenerators[frmt.value] = (src) => { let out = ''; compileStmt.body.forEach(e => { if (e.type === NodeType.String) out += e.value; else if (e.type === NodeType.Variable) { const varExpr = e; const v = src.match(new RegExp(regexes[varExpr.value].source, 'g'))[varExpr.index]; if (v === undefined) throw new CompilerError(compileStmt.range, 'Unknown statement/expression.'); out += v; } else if (e.type === NodeType.WhitespaceIdentifier) out += ' '; }); return out; }; }); } else if (stmt.type === NodeType.Imports) { const importStmt = stmt; importStmt.formats.forEach(frmt => { if (operatorStmtExport.imports[frmt.value] !== undefined) throw new CompilerError(importStmt.range, `Duplicate file format at imports statement \'${frmt}\'`); operatorStmtExport.imports[frmt.value] = importStmt.module.value; }); } else throw new CompilerError(stmt.range, `Unexpected \'${stmt.type}\' statement insdie operator statement.`); }); out.push(operatorStmtExport); } else if (statementIsA(statement, NodeType.Function)) { const statementExport = { type: ExportType.Function, args: statement.arguments.map(s => regexes[s.value]), name: statement.name.value, formatNames: {}, imports: {} }; statement.body.forEach(stmt => { if (statementIsA(stmt, NodeType.Compile)) { if (stmt.body[0].type !== NodeType.String) throw new CompilerError(stmt.range, 'Expected a string after compile statement parens'); stmt.formats.forEach(each => { if (statementExport.formatNames[each.value] !== undefined) throw new CompilerError(stmt.range, `Encountered multiple compile statements for target language '${each}'`); statementExport.formatNames[each.value] = stmt.body[0].value; }); } else if (statementIsA(stmt, NodeType.Imports)) { stmt.formats.forEach(each => { if (statementExport.imports[each.value] !== undefined) throw new CompilerError(stmt.range, `Encountered multiple import statements for target language '${each}'`); statementExport.imports[each.value] = stmt.module.value; }); } }); out.push(statementExport); } else if (statementIsA(statement, NodeType.Keyword)) { out.push({ type: ExportType.Keyword, word: statement.word.value }); } else if (statementIsA(statement, NodeType.Global)) { //TODO } else throw new CompilerError(statement.range, `Unexpected \'${statement.type}\' statement after export statement.`, file); }); this.exportData[file] = out; } /** * Compiles every .sys file found in the given folder. * @param {string} folderPath Folder path to search for .sys files. * @author efekos * @version 1.0.0 * @since 0.0.1-alpha */ compileSysFiles(folderPath) { const files = readdirSync(folderPath); files.forEach(f => { if (f.endsWith('.sys')) this.compileSys(join(folderPath, f)); else if (statSync(join(folderPath, f)).isDirectory()) this.compileSysFiles(join(folderPath, f)); }); } /** * Compiles a .sys file at the path given. * @param {string} file Path to the .sys file to compile. * @author efekos * @since 0.0.1-alpha * @version 1.0.3 */ compileSys(file) { const ast = sysparser.parseTokens(tokenizeSys(readFileSync(file).toString()), file); //# Handle import statements var imported = []; ast.body.forEach(stmt => { if (stmt.type === NodeType.Import) { const importStmt = stmt; const pathToImport = join(dirname(file), importStmt.path.value.endsWith('.syx') ? importStmt.path.value : importStmt.path.value + '.syx'); if (!existsSync(pathToImport)) throw new CompilerError(importStmt.range, `File \'${pathToImport}\' imported from \'${file}\' does not exist.`); this.exportData[pathToImport].forEach(exported => { if (exported.type === ExportType.Operator) if (imported.filter(r => r.type === ExportType.Operator).some(i => exported.regexMatcher === i.regexMatcher)) throw new CompilerError(importStmt.range, `There are more than one operators with the same syntax imported to \'${file}\'.`); imported.push(exported); }); } }); //# Get the actual file content to compile const src = readFileSync(file).toString().split(''); while (src.length > 0 && `${src[0]}${src[1]}${src[2]}` !== ':::') { src.shift(); } src.shift(); src.shift(); src.shift(); let fileContent = src.join(''); const imports = []; //# Compile imported.forEach(i => { if (i.type === ExportType.Operator) { if (i.outputGenerators[this.mainFileFormat] === undefined) throw new CompilerError({ end: { character: 0, line: 0 }, start: { character: 0, line: 0 } }, `Can't compile operator to target language (${this.mainFileFormat}).`); fileContent = fileContent.replace(new RegExp(i.regexMatcher.source, 'g'), i.outputGenerators[this.mainFileFormat]); if (i.imports[this.mainFileFormat] !== undefined && !imports.includes(i.imports[this.mainFileFormat])) imports.push(i.imports[this.mainFileFormat]); } else if (i.type === ExportType.Function) { if (i.formatNames[this.mainFileFormat] === undefined) throw new CompilerError({ end: { character: 0, line: 0 }, start: { character: 0, line: 0 } }, `Can't compile function to target language (${this.mainFileFormat}).`); fileContent = fileContent.replace(new RegExp(i.name + '\\(' + i.args.map(m => m.source).join(',') + '\\)', 'g'), (m) => m.replace(i.name, i.formatNames[this.mainFileFormat])); } }); writeFileSync(file.replace(this.rootDir, this.outDir).replace(/\.[^/.]+$/, '') + '.' + this.mainFileFormat, imports.map(i => `import ${i}`).join('\n') + '\n' + fileContent); } } /** * Type of something that can be exported. * @version 1.0.1 * @since 0.0.1-alpha * @author efekos */ export var ExportType; (function (ExportType) { /** * {@link ExportedOperator}. */ ExportType[ExportType["Operator"] = 0] = "Operator"; /** * {@link ExportedFunction}. */ ExportType[ExportType["Function"] = 1] = "Function"; /** * {@link ExportedKeyword}. */ ExportType[ExportType["Keyword"] = 2] = "Keyword"; /** * {@link ExportedGlobal}. */ ExportType[ExportType["Global"] = 3] = "Global"; })(ExportType || (ExportType = {})); export const regexes = { /** * Regex for `int` primitive type. `int`s can be any number that does not contain fractional digits. * @author efekos * @since 0.0.1-alpha * @version 1.0.0 */ int: /([0-9]+)/, /** * Regex used for `string` primitive type. `string`s are phrases wrapped with quotation marks that can contain anything. * @author efekos * @since 0.0.1-alpha * @version 1.0.0 */ string: /('[\u0000-\uffff]*'|"[\u0000-\uffff]*")/, /** * Regex used for `boolean` primitive type. `boolean`s are one bit, but 0 is represented as `false` and 1 is `false`. * @author efekos * @since 0.0.1-alpha * @version 1.0.0 */ boolean: /(true|false)/, /** * Regex used for `decimal` primitive type. `decimal`s are either integers or numbers with fractional digits. * @author efekos * @since 0.0.1-alpha * @version 1.0.0 */ decimal: /([0-9]+(\.[0-9]+)?)/, /** * Regex used for whitespace identifiers, an identifier used to reference any amount of spaces. * @author efekos * @version 1.0.0 * @since 0.0.1-alpha */ '+s': /\s*/ }; /** * Escapes every RegExp character at the source string. * @param src Source string. * @returns Same string with every RegExp character replaced with '\\$&'. * @author efekos * @version 1.0.0 * @since 0.0.1-alpha */ export function escapeRegex(src) { return src.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } export var CompilerFunctions; (function (CompilerFunctions) { /** * Generates {@link RegExp} of the given operator statement. * @param statement An operator statement. * @returns A regular expression generated from regex of the operator statement. * @author efekos * @version 1.0.0 * @since 0.0.2-alpha */ function generateRegexMatcher(statement) { let regexMatcher = new RegExp(''); statement.regex.forEach(regexStatement => { if (regexStatement.type === NodeType.PrimitiveType) { regexMatcher = new RegExp(regexMatcher.source + regexes[regexStatement.value].source); } if (regexStatement.type === NodeType.WhitespaceIdentifier) { regexMatcher = new RegExp(regexMatcher.source + regexes['+s'].source); } if (regexStatement.type === NodeType.String) { regexMatcher = new RegExp(regexMatcher.source + escapeRegex(regexStatement.value)); } }); return regexMatcher; } CompilerFunctions.generateRegexMatcher = generateRegexMatcher; })(CompilerFunctions || (CompilerFunctions = {}));