@syntaxs/compiler
Version:
Compiler used to compile Syntax Script projects.
315 lines (314 loc) • 14.1 kB
JavaScript
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 = {}));