Компілятор — це програма, яка перетворює вихідний код, написаний мовою програмування високого рівня, у виконуваний машинний код (або іншу мову, у нашому прикладі — Java), який можна запустити на комп’ютері.
Типовий компілятор складається з декількох компонентів або фаз, які працюють разом для перетворення вихідного коду програми у виконуваний код. Ось основні компоненти компілятора:
Ми створимо мову з назвою L і скомпілюємо її до Java-мішені.
Ми будемо використовувати ANTLR.
ANTLR (ANother Tool for Language Recognition) — це потужний генератор, який можна використовувати для створення синтаксичних аналізаторів, лексем і компіляторів для мов програмування, конфігураційних файлів, форматів даних та інших форматів структурованого тексту. Він використовує комбінацію граматик лексем і синтаксичних аналізаторів для аналізу вхідного тексту і створення абстрактних синтаксичних дерев (АСТ), які можуть бути використані компіляторами або інтерпретаторами для виконання програми.
Детальний посібник з налаштування та використання ANTLR тут: Java з ANTLR | Baeldung.
Приклад коду для нашої мови наведено нижче:
def mul[x: Int, y: Int]: Int <- x * y;
var x: Int <- 2;
var y: Int;
y <- x * 2 + x;
mul[x, y+2];
Ми можемо визначити правила для цієї мови у файлі ANTLR g4:
grammar L;
l: statement*
;
statement: function_def
| variable_def
| variable_assign
| function_call
;
function_def : 'def' ID '[' parameters? ']' ':' type assign SC
;
parameters : parameter (',' parameter)*
;
parameter : ID ':' type
;
function_call : ID '[' arguments? ']' SC
;
arguments : argument (',' argument)*
;
argument : expression
;
variable_def : 'var' ID ':' type assign? SC
;
variable_assign : ID assign SC
;
assign: '<-' expression
;
type : 'Int' | 'String' | 'Boolean' | ID
;
expression : value
| ID
| '(' expression ')'
| expression '+' expression
| expression '-' expression
| expression '*' expression
| expression '/' expression
| expression '>' expression
| expression '<' expression
| expression '>=' expression
| expression '<=' expression
| expression '==' expression
| expression '!=' expression
| '!' expression
| expression '&&' expression
| expression '||' expression
;
value : INT
| STRING
| TRUE
| FALSE
;
INT : ('0'..'9')+
;
STRING : '"' (ESC | ~["\\])* '"'
;
fragment ESC : '\\' ["\\/bfnrt]
;
TRUE : 'true'
;
FALSE : 'false'
;
ID : [a-zA-Z][a-zA-Z0-9]*
;
SC : ';'
;
WS : [ \t\r\n]+ -> skip
;
l
: Це початкове правило, якому відповідає нуль або більше операторів.statement
: Відповідає одному з чотирьох типів операторів: визначенням функцій, визначенням змінних, присвоюванням змінних або викликам функцій.function_def:
Відповідає визначенню функції, яке складається з імені, необов’язкових параметрів, типу повернення і тіла. Тіло — це вираз, який присвоюється імені функції.parameters
: Відповідає списку параметрів функції, розділених комами.parameter
: Відповідає одному параметру, який складається з імені, двокрапки та типу.function_call
: Відповідає виклику функції, який складається з імені функції та нуля або більше аргументів.arguments
: Відповідає списку аргументів для виклику функції, розділених комами.variable_def
: Відповідає визначенню змінної, яке складається з імені, двокрапки, типу та необов’язкового присвоєння.variable_assign
: Відповідає присвоюванню змінної, яке складається з імені змінної та виразу, який їй присвоюється.assign
: Відповідає оператору присвоєння та виразу.type
: Відповідає типу, яким може бути Int, String, Boolean або будь-який інший ідентифікатор.expression
: Відповідає базовому виразу, яким може бути значення, змінна, вираз у круглих дужках або комбінація виразів з використанням арифметичних, логічних операторів або операторів порівняння.value
: Відповідає буквальному значенню, яке може бути цілим числом, рядком, істиною або хибністю.Токени:
INT
: Збігається з цілочисельним літералом.STRING
: Відповідає рядковому літералу, який може містити екрановані послідовності.ESC
: Відповідає екранованим послідовностям всередині рядкового літерала.TRUE і FALSE
: Відповідають булевим літералам true і false.ID
: Відповідає ідентифікатору, який використовується для імен змінних, функцій та типів.SC
: Відповідає крапці з комою, яка використовується для завершення операторів.WS
: Збігається з пробілами і пропускає їх.Наш AST має інтерфейс:
public interface Statement {
public String getType();
}
І реалізує його наступне
public class VariableAssign implements Statement {
private Identifier identifier;
private Assign assign;
public String getType() {
return "variable_assign";
}
}
public class VariableDef implements Statement {
private Identifier identifier;
private Boolean initialized;
private Assign assign;
public String getType() {
return "variable_def";
}
}
public class FunctionCall implements Statement {
String name;
List<Argument> arguments;
public String getType() {
return "function_call";
}
}
public class FunctionDef implements Statement {
String name;
List<Parameter> parameters;
Assign assign;
public String getType() {
return "function_def";
}
}
Це класи Java, які реалізують інтерфейс Statement
і представляють різні типи операторів, які можна розбирати з мови L.
VariableAssign
— це оператор, який присвоює значення змінній. Він містить об’єкт Identifier
для змінної, що присвоюється, і об’єкт Assign
для виразу присвоєння.
VariableDef
— це оператор, який визначає змінну. Містить об’єкт Identifier для змінної, що визначається, логічний прапорець, який вказує, чи змінна ініціалізована, та об’єкт Assign для виразу присвоєння (якщо змінна ініціалізована).
FunctionCall
представляє інструкцію, яка викликає функцію. Він містить ім’я функції та список об’єктів Argument
для аргументів функції.
FunctionDef
— це оператор, який визначає функцію. Він містить ім’я функції, список об’єктів Parameter
для параметрів функції та об’єкт Assign
для тіла функції.
Повний код AST — за посиланням.
Цей код є реалізацією відвідувача ANTLR4 для мови програмування. Відвідувач аналізує вхідний код і будує список операторів, які потім можуть бути використані для подальшого аналізу або виконання.
public class LVisitor extends LBaseVisitor {
public List<Statement> statements = new LinkedList<>();
ScopeService scope = new ScopeService();
public Object visitStatement(LParser.StatementContext ctx) {
Object result = super.visitStatement(ctx);
statements.add((Statement) result);
return result;
}
public Statement visitVariable_def(LParser.Variable_defContext ctx) {
VariableDef.VariableDefBuilder builder = VariableDef.builder();
// Add the identifier
Identifier identifier = new Identifier(ctx.ID().getText(), new Type(ctx.type().getText()));
scope.addIdentifier(identifier);
builder.identifier(identifier);
// Add the assign if exists
if (ctx.assign() != null) {
Assign assign = buildAssign(ctx.assign(), identifier.getType());
builder.initialized(true);
builder.assign(assign);
}
return builder.build();
}
public Statement visitVariable_assign(LParser.Variable_assignContext ctx) {
Identifier identifier = scope.getIdentifier(ctx.ID().getText());
Assign assign = buildAssign(ctx.assign(), identifier.getType());
return new VariableAssign(identifier, assign);
}
public Statement visitFunction_def(LParser.Function_defContext ctx) {
String name = ctx.ID().getText();
List<Parameter> parameters = new LinkedList<>();
scope.openScope();
for (int i = 0; i < ctx.parameters().parameter().size(); i++) {
LParser.ParameterContext pc = ctx.parameters().parameter().get(i);
Identifier identifier = new Identifier(pc.ID().getText(), new Type(pc.type().getText()));
scope.addIdentifier(identifier);
parameters.add(new Parameter(identifier, i));
}
Assign assign = buildAssign(ctx.assign(), new Type(ctx.type().getText()));
scope.closeScope();
FunctionDef def = new FunctionDef(name, parameters, assign);
scope.addFunction(def);
return def;
}
public Statement visitFunction_call(LParser.Function_callContext ctx) {
String name = ctx.ID().getText();
List<Argument> arguments = new LinkedList<>();
for (int i = 0; i < ctx.arguments().argument().size(); i++) {
LParser.ArgumentContext arg = ctx.arguments().argument().get(i);
Expression expression = buildExpression(arg.expression());
arguments.add(new Argument(i, expression));
}
// Validate function exists and it's arguments
scope.getFunctionDef(name, arguments);
return new FunctionCall(name, arguments);
}
private Assign buildAssign(LParser.AssignContext ctx, Type type) {
return new Assign(type, buildExpression(ctx.expression()));
}
private Expression buildExpression(LParser.ExpressionContext ctx) {
if (ctx.value() != null) {
return getValueExpression(ctx.value());
} else if (ctx.ID() != null) {
return getIDExpression(ctx.ID().getText());
} else if (ctx.getChildCount() == 3) {
Expression left = buildExpression(ctx.expression(0));
Expression right = buildExpression(ctx.expression(1));
String operator = ctx.getChild(1).getText();
return getBinaryExpression(operator, left, right);
} else if (ctx.getChildCount() == 2) {
String operator = ctx.getChild(0).getText();
Expression right = buildExpression(ctx.expression(0));
return getUnaryExpression(operator, right);
} else {
return null;
}
}
private Expression getUnaryExpression(String op, Expression rhs) {
Expression.ExpressionBuilder builder = Expression.builder();
builder
.expressionType(Expression.ExpressionType.UNARY)
.op(op)
.rhs(rhs);
return builder.build();
}
private Expression getBinaryExpression(String op, Expression lhs, Expression rhs) {
Expression.ExpressionBuilder builder = Expression.builder();
builder
.expressionType(Expression.ExpressionType.BINARY)
.op(op)
.lhs(lhs)
.rhs(rhs);
return builder.build();
}
private Expression getIDExpression(String name) {
Expression.ExpressionBuilder builder = Expression.builder();
builder
.expressionType(Expression.ExpressionType.ID)
.ID(scope.getIdentifier(name));
return builder.build();
}
private Expression getValueExpression(LParser.ValueContext context) {
Expression.ExpressionBuilder builder = Expression.builder();
builder.expressionType(Expression.ExpressionType.VALUE)
.value(new Value(getValueExpressionType(context), context.getText()));
return builder.build();
}
private Type getValueExpressionType(LParser.ValueContext context) {
if (context.STRING() != null) return new Type("String");
else if (context.INT() != null) return new Type("Int");
else if (context.TRUE() != null || context.FALSE() != null) return new Type("Bool");
else return new Type("String");
}
}
Відвідувач визначає методи для кожного правила граматики ANTLR4, і кожен метод створює відповідний оператор або об’єкт виразу, який потім додається до списку операторів.
Відвідувач також керує областю видимості (таблицею символів), яка використовується для відстеження змінних і функцій, оголошених у програмі. Область видимості оновлюється при оголошенні нових змінних і функцій, а також використовується для пошуку ідентифікаторів, коли на них посилаються в коді (семантичний аналізатор і обробник помилок).
public class Scope {
List<Identifier> identifiers = new LinkedList<>();
List<FunctionDef> functions = new LinkedList<>();
Scope parent = null;
public void addIdentifier(Identifier identifier) {
Optional<Identifier> check = identifiers.stream().filter(i -> i.getName().equals(identifier.getName())).findFirst();
if (check.isPresent()) {
throw new IdentifierAlreadyInScope(identifier.getName());
}
identifiers.add(identifier);
}
public void addFunction(FunctionDef functionDef) {
Optional<FunctionDef> check = functions.stream().filter(i -> i.getName().equals(functionDef.getName())).findFirst();
if (check.isPresent()) {
throw new IdentifierAlreadyInScope(functionDef.getName());
}
functions.add(functionDef);
}
public Identifier getIdentifier(String id) throws IdentifierNotFoundInScope {
Optional<Identifier> identifier = identifiers.stream().filter(i -> i.getName().equals(id)).findFirst();
if (identifier.isPresent()) {
return identifier.get();
}
if (parent != null) {
return parent.getIdentifier(id);
}
throw new IdentifierNotFoundInScope(id);
}
public FunctionDef getFunction(String id, List<Argument> arguments) throws IdentifierNotFoundInScope {
Optional<FunctionDef> function = functions.stream().filter(i -> i.getName().equals(id)).findFirst();
if (function.isPresent()) {
List<Parameter> parameters = function.get().getParameters();
if (!(arguments.size() == parameters.size())) {
// Validate argument count matching parameter count.
throw new ArgumentsNotCompliantWithParametersList(id);
}
for (int i = 0; i < parameters.size(); i++) {
// Validate argument types matching parameter types.
if (!arguments.get(i).getExpression().resultType().equals(parameters.get(i).getIdentifier().getType())) {
throw new ArgumentsNotCompliantWithParametersList(id);
}
}
return function.get();
}
if (parent != null) {
return parent.getFunction(id, arguments);
}
throw new IdentifierNotFoundInScope(id);
}
public void clear() {
identifiers.clear();
}
}
Це клас Java під назвою Scope, який використовується для відстеження змінних і функцій у певній області видимості програми.
Клас має три поля:
Клас має декілька методів:
addIdentifier()
: Додає об’єкт Identifier
до списку ідентифікаторів, але спочатку перевіряє на наявність дублікатів.addFunction()
: Додає об’єкт FunctionDef
до списку функцій, але спочатку перевіряє на наявність дублікатів.getIdentifier()
: Повертає об’єкт Identifier
за його іменем. Якщо ідентифікатор не знайдено у поточній області видимості, він шукається у батьківській області видимості (якщо вона існує). Якщо ідентифікатор все ще не знайдено, генерується виключення IdentifierNotFoundInScope
.getFunction()
: Повертає об’єкт FunctionDef за його іменем та списком аргументів. Перевіряє, чи кількість аргументів відповідає кількості параметрів, і чи типи аргументів відповідають типам параметрів. Якщо функцію не знайдено у поточній області видимості, вона шукається у батьківській області видимості (якщо вона існує). Якщо функцію все одно не знайдено, згенерується виключення IdentifierNotFoundInScope
.clear()
: Очищає список ідентифікаторів. Це використовується для скидання області видимості між викликами функції.public class ScopeService {
Scope currentScope = new Scope();
public void openScope() {
Scope newScope = new Scope();
newScope.setParent(currentScope);
currentScope = newScope;
}
public void closeScope() {
currentScope.clear();
currentScope = currentScope.getParent();
}
public void addIdentifier(Identifier identifier) throws IdentifierAlreadyInScope {
currentScope.addIdentifier(identifier);
}
public Identifier getIdentifier(String id) throws IdentifierNotFoundInScope {
return currentScope.getIdentifier(id);
}
public void addFunction(FunctionDef def) throws IdentifierAlreadyInScope {
currentScope.addFunction(def);
}
public FunctionDef getFunctionDef(String id, List<Argument> arguments) throws IdentifierNotFoundInScope {
return currentScope.getFunction(id, arguments);
}
}
ScopeService
, який надає методи для керування діапазонами та ідентифікаторами всередині них. Клас має змінну екземпляра з назвою currentScope, яка спочатку встановлюється на новий об’єкт Scope
.
Метод openScope
створює новий об’єкт Scope
, встановлює його батька у поточну область видимості і робить нову область видимості поточною.
Метод closeScope
очищає поточну область видимості і встановлює поточну область видимості на її батька.
Метод addIdentifier
додає об’єкт Identifier
до поточної області видимості за допомогою методу addIdentifier
поточної області видимості.
Метод getIdentifier
отримує об’єкт Identifier
за іменем з поточної області видимості за допомогою методу getIdentifier
поточної області видимості.
Метод addFunction
додає об’єкт FunctionDef
до поточної області видимості з допомогою методу addFunction
поточної області видимості.
Метод getFunctionDef
отримує об’єкт FunctionDef
за іменем та аргументами з поточної області видимості методом getFunction
поточної області видимості.
FreeMarker — це движок шаблонів на основі Java, який використовується для створення динамічного контенту, такого як веб-сторінки HTML, електронні листи та документи. Він дозволяє розробникам відокремити рівень презентації від рівня бізнес-логіки за допомогою шаблонів. Шаблони FreeMarker — це, по суті, текстові файли із заповнювачами, які під час обробки шаблону замінюються динамічним контентом.
Детальніше — за посиланням.
Файл цільового шаблону Java:
package samsaydali.l.codegen;
public class Main {
public static void main(String[] args) {
<
<
${statement.identifier.type.type} ${statement.identifier.name}<
</
<
${statement.identifier.name} = ${statement.assign.expression.getText()};
</
<
${statement.name}(${statement.arguments?map(a -> a.expression.getText())?join(', ')?no_esc});
</
</
}
<
private static int ${function.name}(${function.parameters?map(p -> p.identifier.type.type + " " + p.identifier.name)?join(', ')?no_esc}) {
return ${function.assign.expression.getText()};
}
</
}
Шаблон FreeMarker використовується для генерації коду на основі списку операторів. Він містить кілька умовних блоків, які перевіряють тип кожного оператора і генерують відповідний код на основі цього типу.
Наприклад, якщо оператор є визначенням змінної, шаблон генерує рядок коду, який оголошує змінну з вказаним типом та іменем, опціонально ініціалізовану значенням виразу, що стоїть праворуч від знаку рівності. Якщо оператор є викликом функції, шаблон генерує рядок коду, який викликає вказану функцію з заданими аргументами.
Крім того, шаблон містить цикл, який перебирає список операторів і генерує код для кожного визначення функції, знайденого в списку. Кожне визначення функції перетворюється на закритий статичний метод з тим самим іменем, що й функція, з відповідними типами та іменами параметрів, а тіло методу встановлюється у значення виразу, присвоєного функції.
Варто зазначити, що оператори <#if> і <#list> є частиною синтаксису FreeMarker і використовуються тут для умовної генерації коду на основі типу кожного оператора і для циклічного перегляду списку операторів для генерації коду для кожного визначення функції.
public class LCompiler {
public LVisitor visit(String lInput) {
LLexer lexer = new LLexer(CharStreams.fromString(lInput));
CommonTokenStream tokens = new CommonTokenStream(lexer);
LParser parser = new LParser(tokens);
LVisitor visitor = new LVisitor();
parser.l().accept(visitor);
повернути visitor;
}
public List<Statement> getAST(LVisitor visitor) {
повернути visitor.statements;
}
public JavaGenerator getGenerator() {
повернути новий JavaGenerator();
}
public String generateCode(List<Statement> ast) throws TemplateException, IOException {
return getGenerator().generate(ast);
}
public String compileToJava(String lInput) throws TemplateException, IOException {
LVisitor visitor = visit(lInput);
List<Statement> ast = getAST(visitor);
Рядок javaCode = generateCode(ast);
return javaCode;
}
}
Клас LCompiler
є основним класом, що відповідає за компіляцію L-коду в код Java.
Метод visit
приймає вхідні дані L, ініціалізує лексер і синтаксичний аналізатор за допомогою ANTLR і передає розібраний AST екземпляру LVisitor
, який відповідає за відвідування вузлів AST і побудову списку об’єктів Statement
.
Метод getAST
просто повертає список об’єктів Statement
, побудований відвідувачем.
Метод getGenerator
створює і повертає новий екземпляр JavaGenerator
, який є класом, що відповідає за генерацію Java-коду зі списку об’єктів Statement
.
Метод generateCode
отримує список об’єктів Statement
і генерує Java-код за допомогою класу JavaGenerator
.
Нарешті, метод compileToJava
отримує вхідні дані L, компілює їх у список об’єктів Statement
за допомогою visit
, генерує Java-код зі списку об’єктів Statement
за допомогою generateCode
і повертає результуючий Java-код.
Повний код тут.
Цей текст взято з особистого блогу після отримання дозволу автора.
Блогер та розробник Джозеф Круз розповів, чому не варто писати ідеальний код та чому це…
Днями я завзято нила про щось ChatGPT (експериментую між сеансами з живим терапевтом). І от…
«Крутіть колесо, щоб отримати знижку до 50%!» «Натисніть тут, щоб відкрити таємничу пропозицію!» «Зареєструйтесь зараз,…
Дуже хочеться робити якісь десктопні апки. Сумую за часами коли всі програми були offline-first, і…
Надсилаючи криптовалюту, багато новачків ставлять запитання: як працюють комісії та чому вони відрізняються в різних…
Нова афера набирає обертів — ось детальний розбір того, як фальшиві потенційні роботодавці намагаються вкрасти…