GitHub

Прошлые статьи:

Изменения

  1. Объявление трейтов

  2. Реализация методов трейтов для структур

  3. Создание указателей (строки не обрабатываются!)

  4. Разыменование указателей

  5. Получение адреса объекта

  6. Оператор вывода новой строки в консоль (echo)

Планы на будущее

  1. Операторы new/delete

  2. Модули

  3. Статические переменные, методы и поля

  4. Массивы

  5. Оператор sizeof

  6. Явное приведение (C-like синтаксис)

  7. Extern вызовы

Примеры кода

trait TPrintable {
    pub fun Print();
}

struct Human {
    var age: i32;
}

struct Car {
    var power: i32;
}

impl TPrintable for Human {
    pub fun Print() {
        echo this.age; echo '\n';
    }
}

impl TPrintable for Car {
    pub fun Print() {
        echo this.power; echo '\n';
    }
}

fun main(): i32 {
    var h: Human = Human { age: 16 };
    var c: Car = Car { power: 1500 };
    h.Print();
    c.Print();
    print(h);
    return 0;
}

fun print(p: TPrintable) {
    p.Print();
}

// output:
// 16
// 1500
// 16
fun main(): i32 {
    var a: i32;
    if tryInc(&a) {
        echo a; echo '\n';
    }
    return 0;
}

fun tryInc(a: *i32): bool {
    if a != nil {
        *a += 1;
        return true;
    }
    return false;
}

// output:
// 1

Важная просьба

Язык активно улучшается, осталось недолго до bootstrapping'а! Также уменя есть планы писать LSP и загрузить язык на github linguist. Для последнего нужны репозитории (>200). Для тех, кому нечего делать, или кому просто не лень, попробуйте пописать программки на моем языке. Пока мало что можно будет написать, но, думаю, хотя бы что-то можно. Не знаю, что там с виндой, но на линуксе проект точно собирается (нужен лишь clang и cmake). Если встретятся какие-то баги, то сразу создавайте Issue с текстом ошибки и кодом, который привел к этой ошибке. Я буду очень рад, если язык начнет обрастать хотя бы минимальным комьюнити :)

Контент

Итак, продолжаем работу над моим языком! В этот раз не так много новшеств, однако, они очень интересные и сложные. Начнем с легкого: с вывода в консоль.

Вывод в консоль (echo)

echo - кейворд, который нужен для конструкций вида echo <expr>. Echo выводит значение выражения в строку, не добавляя в конце \n. Какой-то специфичной семантики у него нет - он просто вызывает Visit для выражения.

Кодген
llvm::Value *
CodeGen::VisitEchoStmt(EchoStmt *es) {
    std::string format;
    llvm::Value *val = Visit(es->GetRHS());
    llvm::Type *type = val->getType();
    if (type->isIntegerTy()) {
        unsigned bitWidth = type->getIntegerBitWidth();

        switch (bitWidth) {
            case 1: {
                llvm::Constant *trueStr = _builder.CreateGlobalStringPtr("true\n", "str.true");
                llvm::Constant *falseStr = _builder.CreateGlobalStringPtr("false\n", "str.false");

                llvm::Value *selectedStr = _builder.CreateSelect(val, trueStr, falseStr, "bool.str");

                llvm::Constant *fmtStr = _builder.CreateGlobalStringPtr("%s", "printf.format");

                _builder.CreateCall(_module->getFunction("printf"), { fmtStr, selectedStr });
                return nullptr;
            }
            case 8:
                format = "%c";
                break;
            case 32:
                format = "%d";
                break;
            case 64:
                format = "%lld";
                break;
            default:
                val = implicitlyCast(val, _builder.getInt64Ty());
                format = "%lld";
                break;
        }
    }
    else if (type->isFloatingPointTy()) {
        format = "%g";
        if (type->isFloatTy()) {
            val = _builder.CreateFPExt(val, llvm::Type::getDoubleTy(_context));
        }
    }
    else if (type->isPointerTy()) {
        format = "%p";
    }
    _builder.CreateCall(_module->getFunction("printf"), { _builder.CreateGlobalString(format, "printf.format"), val });
    return nullptr;
}

Echo для вывода использует printf, поэтому не забудем сделать декларацию в DeclareFunctionsAndStructures. На будущее ещё добавим декларацию функции abort.

printf & abort
void
CodeGen::DeclareFunctionsAndStructures(std::vector<Stmt *> &ast) {
    llvm::FunctionType *printfType = llvm::FunctionType::get(llvm::Type::getInt32Ty(_context), { llvm::PointerType::get(_context, 0) }, true);
    llvm::Function *printfFun = llvm::Function::Create(printfType, llvm::GlobalValue::ExternalLinkage, "printf", *_module);

    llvm::FunctionType *abortTy = llvm::FunctionType::get(_builder.getVoidTy(), false);
    llvm::Function *abortFun = llvm::Function::Create(abortTy, llvm::GlobalValue::ExternalLinkage, "abort", *_module);

    // ...
}

Вернемся к кодгену для echo. Как мы знаем для вывода printf требует строку формата, здесь мы ее и сгенерируем. Логика предельно простая:

  1. Если выражение - bool, то выводим true/false, вместо 1/0, как в си

  2. Если выражение - целое число, генерируем форматный вывод целого числа (в зависимости от размера числа)

  3. Если выражение - дробное число, то генерируем формат %g

  4. Если выражение - указатель, генерируем формат %p

Затем вызываем printf.

Трейты

Это не супер сложная часть, но обо всём по порядку. Представьте, что у вас есть объекты, которые могут, например, переводить себя в строку. Такие объекты обладают одним и тем же методом ToString с одной и той же сигнатурой. Почему бы не объеденить такие объекты в какую-то некую общую группу? Именно для этого и нужны трейты. Они объединяют пользовательские объекты в какие-то общие группы, добавляя черты поведения объектам. Трейт - набор методов без реализации (без блока). При реализации методов трейта в моём языке вы обязаны реализовать их все.

Помимо всего этого у трейтов есть одна замечательная способность - упаковка и распаковка. Суть заключается в том, чтобы превратить структуру, которая реализует какой-то трейт, в этот самый трейт (упаковка), при этом реализация методов для трейта останется той же. В слуаче с распаковкой происходит всё наоборот - трейт конвертируется в структуру (с условием, что этот трейт был получен путём упаковки той же структуры, в которую мы пытаемся конвертировать трейт). На данный момент в языке доступная только упаковка трейтов. Распаковка трейтов появится тогда, когда будут внедряться операторы явного приведения.

Нам нужно расширить синтаксис имплементаций, чтобы можно было писать не только impl Struct, но и impl Trait for Struct.

Обновление AST
// ...

namespace marble {
    class ImplStmt : public Stmt {
        // раньше был просто _name
        std::string _traitName;
        std::string _structName;
        // ...
    };
}
#pragma once
#include <marble/AST/Stmt.h>
#include <llvm/ADT/StringRef.h>
#include <vector>

namespace marble {
    class TraitDeclStmt : public Stmt {
        std::string _name;
        std::vector<Stmt *> _body;

    public:
        explicit TraitDeclStmt(std::string name, std::vector<Stmt *> body, AccessModifier access, llvm::SMLoc startLoc, llvm::SMLoc endLoc)
                             : _name(name), _body(body), Stmt(NkTraitDeclStmt, access, startLoc, endLoc) {}

        constexpr static bool
        classof(const Node *node) {
            return node->GetKind() == NkTraitDeclStmt;
        }
        
        std::string
        GetName() const {
            return _name;
        }

        std::vector<Stmt *>
        GetBody() const {
            return _body;
        }
    };
}

При имплементации трейта программист обязан реализовать все его методы, поэтому при имплементации нужно проверять, какие методы находятся в блоке имплементаций. Если там есть метод, который не объявлен в трейте, то также нужно выкинуть ошибку. Парсер по традиции мы не рассматриваем из-за простоты изменений (гляньте гитхаб, если всё-таки нужен код парсера).

Семантика
std::optional<ASTVal>
SemanticAnalyzer::VisitImplStmt(ImplStmt *is) {
    if (_vars.size() != 1) {
        _diag.Report(is->GetStartLoc(), ErrCannotBeHere)
            << llvm::SMRange(is->GetStartLoc(), is->GetEndLoc());
    }
    if (_structs.find(is->GetStructName()) == _structs.end()) {
        _diag.Report(is->GetStartLoc(), ErrUndeclaredStructure)
             << llvm::SMRange(is->GetStartLoc(), is->GetEndLoc())
             << is->GetStructName();
        return std::nullopt;
    }
    Struct s = _structs.at(is->GetStructName());
    bool isTraitImpl = !is->GetTraitName().empty();
    const Trait *traitDef = nullptr;
    std::unordered_map<std::string, bool> implementedTraitMethods;
    
    if (isTraitImpl) {
        auto tIt = _traits.find(is->GetTraitName());
        if (tIt == _traits.end()) {
            _diag.Report(is->GetStartLoc(), ErrUndeclaredTrait)
                << llvm::SMRange(is->GetStartLoc(), is->GetEndLoc())
                << is->GetTraitName();
            return std::nullopt;
        }
        traitDef = &tIt->second;
        
        for (auto &method : traitDef->Methods) {
            implementedTraitMethods[method.first] = !method.second.Fun.IsDeclaration;
        }
    }

    std::vector<FunDeclStmt *> methods;
    for (auto &stmt : is->GetBody()) {
        if (stmt->GetKind() != NkFunDeclStmt) {
            _diag.Report(stmt->GetStartLoc(), ErrCannotBeHere)
                << llvm::SMRange(stmt->GetStartLoc(), stmt->GetEndLoc());
            continue;
        }
        FunDeclStmt *method = llvm::dyn_cast<FunDeclStmt>(stmt);
        if (method->IsDeclaration()) {
            _diag.Report(method->GetStartLoc(), ErrCannotDeclareHere)
                << llvm::SMRange(method->GetStartLoc(), method->GetEndLoc());
            continue;
        }
        if (s.Methods.find(method->GetName()) != s.Methods.end()) {
            _diag.Report(stmt->GetStartLoc(), ErrRedefinitionMethod)
                << llvm::SMRange(stmt->GetStartLoc(), stmt->GetEndLoc())
                << method->GetName();
            continue;
        }
        if (isTraitImpl) {
            auto tMethodIt = traitDef->Methods.find(method->GetName());
            if (tMethodIt == traitDef->Methods.end()) {
                _diag.Report(method->GetStartLoc(), ErrMethodNotInTrait)
                    << llvm::SMRange(method->GetStartLoc(), method->GetEndLoc())
                    << method->GetName()
                    << traitDef->Name;
                continue;
            }

            const Function &traitFun = tMethodIt->second.Fun;
            if (method->GetRetType() != traitFun.RetType) {
                _diag.Report(method->GetStartLoc(), ErrCannotImplTraitMethod_RetTypeMismatch)
                    << llvm::SMRange(method->GetStartLoc(), method->GetEndLoc())
                    << method->GetName()
                    << traitDef->Name
                    << traitFun.RetType.ToString()
                    << method->GetRetType().ToString();
            }
            if (method->GetArgs().size() != traitFun.Args.size()) {
                 _diag.Report(method->GetStartLoc(), ErrCannotImplTraitMethod_FewArgs)
                    << llvm::SMRange(method->GetStartLoc(), method->GetEndLoc())
                    << method->GetName()
                    << traitDef->Name
                    << traitFun.Args.size()
                    << method->GetArgs().size();
            }
            else {
                for (int i = 0; i < method->GetArgs().size(); ++i) {
                    if (method->GetArgs()[i].GetType() != traitFun.Args[i].GetType()) {
                        _diag.Report(method->GetStartLoc(), ErrCannotImplTraitMethod_ArgTypeMismatch)
                            << llvm::SMRange(method->GetStartLoc(), method->GetEndLoc())
                            << method->GetArgs()[i].GetName()
                            << traitDef->Name
                            << method->GetArgs()[i].GetName()
                            << traitFun.Args[i].GetType().ToString()
                            << method->GetArgs()[i].GetType().ToString(); 
                    }
                }
            }

            implementedTraitMethods[method->GetName()] = true;
        }
        methods.push_back(method);
        Function fun { .Name = method->GetName(), .RetType = method->GetRetType(), .Args = method->GetArgs(), .Body = method->GetBody(),
                       .IsDeclaration = method->IsDeclaration() };
        s.Methods.emplace(method->GetName(), Method { .Fun = fun, .Access = method->GetAccess() });
    }

    if (isTraitImpl) {
        for (auto const &[name, implemented] : implementedTraitMethods) {
            if (!implemented) {
                _diag.Report(is->GetStartLoc(), ErrNotImplTraitMethod)
                    << llvm::SMRange(is->GetStartLoc(), is->GetEndLoc())
                    << name
                    << traitDef->Name;
            }
        }
        s.TraitsImplements.emplace(traitDef->Name, *traitDef);
    }
    _structs.at(is->GetStructName()).Methods = s.Methods;
    _structs.at(is->GetStructName()).TraitsImplements = s.TraitsImplements;

    for (auto &method : methods) {
        _vars.push({});
        ASTType thisType = ASTType(ASTTypeKind::Struct, s.Name, false, 0);
        _vars.top().emplace("this", Variable { .Name = "this", .Type = thisType, .Val = ASTVal::GetDefaultByType(thisType), .IsConst = false });
        for (auto arg : method->GetArgs()) {
            if (_vars.top().find(arg.GetName()) != _vars.top().end()) {
                _diag.Report(method->GetStartLoc(), ErrRedefinitionVar)
                    << llvm::SMRange(method->GetStartLoc(), method->GetEndLoc())
                    << arg.GetName();
            }
            _vars.top().emplace(arg.GetName(), Variable { .Name = arg.GetName(), .Type = arg.GetType(),
                                                          .Val = arg.GetType().IsPointer() ? ASTVal(arg.GetType(), ASTValData { .i32Val = 0 }, false)
                                                                                           : ASTVal::GetDefaultByType(arg.GetType()),
                                                          .IsConst = arg.GetType().IsConst() });
        }
        _funRetsTypes.push(method->GetRetType());
        bool hasRet;
        for (auto stmt : method->GetBody()) {
            if (stmt->GetKind() == NkRetStmt) {
                hasRet = true;
            }
            Visit(stmt);
        }
        _funRetsTypes.pop();
        _vars.pop();

        if (!hasRet && method->GetRetType().GetTypeKind() != ASTTypeKind::Noth) {
            _diag.Report(method->GetStartLoc(), ErrNotAllPathsReturnsValue)
                << llvm::SMRange(method->GetStartLoc(), method->GetEndLoc());
        }
    }
    return std::nullopt;
}

Много, знаю, но щас всё объясню. Большая часть кода - уже существовавшая реализация VisitImplStmt. Для начала нужно проверить, является ли имплементация вообще имплементацией трейта. Для этого проверяем, не пустое ли имя трейта в ImplStmt. Если всё-таки это имплементация трейта, то находим трейт и запоминаем его методы, чтобы потом понять, реализованы ли они все. Во время анализа тела ImplStmt снова проверяем, создается ли имплементация трейта. Если да, то пытаемся найти метод в трейте (если не нашли - ошибка) и сравниваем сигнатуру текущей реализации с ожидаемой в трейте (тип возвращаемого значения и типы аргументов). Под конец проверяем, все ли методы трейта реализованы, а потом проверяем код тела метода.

Семантика упаковки структур в трейты
ASTVal
SemanticAnalyzer::implicitlyCast(ASTVal src, ASTType expectType, llvm::SMLoc startLoc, llvm::SMLoc endLoc) const {
    if (src.GetType() == expectType) {
        return src;
    }
    
    // тут был код для указателей, его пока не рассматриваем

    if (!expectType.IsPointer() && expectType.GetTypeKind() == ASTTypeKind::Trait &&
        src.GetType().GetTypeKind() == ASTTypeKind::Struct) {
        Struct s = _structs.at(src.GetType().GetVal());
        if (s.TraitsImplements.find(expectType.GetVal()) != s.TraitsImplements.end()) {
            return ASTVal::GetVal(0, expectType);
        }
    }
    // ...
}

Теперь кодген и тут нужно будет разъяснить ещё несколько моментов. В рамках low-level не существует такого понятия как трейт. Структура упаковывается в трейт, но как передать её? Трейт ведь может реализовывать не одна структура. Ответ есть, даже два:

  1. Статическая мономорфизация

  2. Динамическая мономорфизация

Сейчас подробно разберемся с каждым.

Статическая мономорфизация

Сама суть мономорфизации заключается в генерации кода под все возможные варианты. Конкретно со статической мономорфизацией всё просто. Представьте, что есть какая-то функция print которая принимает аргумент p: TPrintable. TPrintable - трейт, который реализует 3 структуры: Circle, Rect и Triangle. Представьте, что мы создали экземпляры Circle и Rect и два раза вызвали print, передав сначала Circle, а затем Rect. Вот код:

trait TPrintable {
    pub fun Print();
}

struct Circle {
    pub var r: f64;
}

struct Rect {
    pub var w: f64;
    pub var h: f64;
}

struct Triangle {
    pub var a: f64;
    pub var b: f64;
    pub var c: f64;
}

impl TPrintable for Circle {
    pub fun Print() {
        echo this.r; echo '\n';
    }
}

impl TPrintable for Rect {
    pub fun Print() {
        echo this.w; echo ' ';
        echo this.h; echo '\n';
    }
}

impl TPrintable for Circle {
    pub fun Print() {
        echo this.a; echo ' ';
        echo this.b; echo ' ';
        echo this.c; echo '\n';
    }
}

fun main(): i32 {
    var c: Circle = Circle { r: 2 };
    var r: Rect = Rect { w: 2, h: 5 };
    print(c);
    print(r);
    return 0;
}

fun print(p: TPrintable) {
    p.Print();
}

Этот код будет применяться и для описания динамической мономорфизации. Смысл в том, чтобы упаковать структуры в TPrintable �� вызвать метод Print, не потеряв код реализации. Звучит легко, на деле пиз... Если мы реализовываем статическую мономорфизацию, то должны будем создать копию функции с трейтом для всех возможных структур, которые мы передадим ей. Я не просто так сказал, что в функцию мы передадим только две структуры из трех, которые реализуют TPrintable. Кодген должен будет сделать копию print для Circle и Rect вместо TPrintable в аргументе. На вызове print(c) компилятор понимает, что вызывает копию с Circle и вызывает именно её. Аналогично для Rect-копии. Этот подход не сложный и чертовски быстрый. Однако у такого способа есть ключевой недостаток: сами копии. Вы же понимаете, что каждая копия функции - место в итоговом бинарнике? Бинарь после таких махинаций получается очень жирный, что не есть хорошо. Этим подходом, например, пользуется C++ для своих дженериков.

Динамическая мономорфизация

Это уже более современный подход, который использует тот же Rust для динамических трейтов. Этот метод довольно сильно отличается от предыдущего. Вместо того, чтобы плодить миллион функций, мы поступим хитрее - создадим структуру, которая будет описывать трейт в рамках low-level. Это по другому называется fat pointer (жирный указатель). Название говорит за себя - структура, которая описывает трейт, хранит два поля (указателя): data (хранит адрес упакованной структуры), vtable (массив реализаций методов трейта для конкретной структуры из data). Суть в том, чтобы представить трейт как эту структуру и передавать её в качестве аргумента. Внутри функции компилятор поймет, из како�� виртуальной таблицы методов достать определенный метод для вызова. Также компилятор достанет из этой же структуры-обертки трейта и саму базовую структуру, а затем просто вызовет метод, полученный из vtable, и передаст ему полученную структуру. Никаких копий. Почти без потерь в производительности. Думаю, выбор очевиден.

DeclareFunctionsAndStructures
void
CodeGen::DeclareFunctionsAndStructures(std::vector<Stmt *> &ast) {
    llvm::FunctionType *printfType = llvm::FunctionType::get(llvm::Type::getInt32Ty(_context), { llvm::PointerType::get(_context, 0) }, true);
    llvm::Function *printfFun = llvm::Function::Create(printfType, llvm::GlobalValue::ExternalLinkage, "printf", *_module);

    llvm::FunctionType *abortTy = llvm::FunctionType::get(_builder.getVoidTy(), false);
    llvm::Function *abortFun = llvm::Function::Create(abortTy, llvm::GlobalValue::ExternalLinkage, "abort", *_module);

    for (auto &stmt : ast) {
        if (FunDeclStmt *fds = llvm::dyn_cast<FunDeclStmt>(stmt)) {
            std::vector<llvm::Type *> args(fds->GetArgs().size());
            std::vector<ASTType> argsAST(fds->GetArgs().size());
            for (int i = 0; i < fds->GetArgs().size(); ++i) {
                args[i] = typeToLLVM(fds->GetArgs()[i].GetType());
                argsAST[i] = fds->GetArgs()[i].GetType();
            }
            llvm::FunctionType *retType = llvm::FunctionType::get(typeToLLVM(fds->GetRetType()), args, false);
            llvm::Function *fun = llvm::Function::Create(retType, llvm::GlobalValue::ExternalLinkage, fds->GetName(), *_module);
            
            if (fds->GetRetType().GetTypeKind() == ASTTypeKind::Struct) {
                llvm::MDNode *metadata = llvm::MDNode::get(_context, llvm::MDString::get(_context, fds->GetRetType().GetVal()));
                fun->setMetadata("struct_name", metadata);
            }
            _functions.emplace(fun->getName(), fun);
            _funArgsTypes.emplace(fun->getName().str(), argsAST);
        }
        else if (ImplStmt *is = llvm::dyn_cast<ImplStmt>(stmt)) {
            Struct &s = _structs.at(is->GetStructName());
            if (!is->GetTraitName().empty()) {
                s.TraitsImplements.emplace(is->GetTraitName(), _traits.at(is->GetTraitName()));
            }
            for (auto stmt : is->GetBody()) {
                FunDeclStmt *fds = llvm::cast<FunDeclStmt>(stmt);
                std::vector<llvm::Type *> args(fds->GetArgs().size() + 1);
                std::vector<ASTType> argsAST(fds->GetArgs().size() + 1);
                args[0] = llvm::PointerType::get(_structs.at(is->GetStructName()).Type, 0);
                argsAST[0] = ASTType(ASTTypeKind::Struct, is->GetStructName(), true, 0);
                for (int i = 0; i < fds->GetArgs().size(); ++i) {
                    args[i + 1] = typeToLLVM(fds->GetArgs()[i].GetType());
                    argsAST[i + 1] = fds->GetArgs()[i].GetType();
                }
                llvm::FunctionType *retType = llvm::FunctionType::get(typeToLLVM(fds->GetRetType()), args, false);
                llvm::Function *fun = llvm::Function::Create(retType, llvm::GlobalValue::ExternalLinkage, is->GetStructName() + "." + fds->GetName(), 
                                                             *_module);
                
                if (fds->GetRetType().GetTypeKind() == ASTTypeKind::Struct) {
                    llvm::MDNode *metadata = llvm::MDNode::get(_context, llvm::MDString::get(_context, fds->GetRetType().GetVal()));
                    fun->setMetadata("struct_name", metadata);
                }
                llvm::MDNode *metadata = llvm::MDNode::get(_context, llvm::MDString::get(_context, is->GetStructName()));
                fun->setMetadata("this_struct_name", metadata);
                _functions.emplace(fun->getName(), fun);
                _funArgsTypes.emplace(fun->getName().str(), argsAST);
            }
        }
        else if (StructStmt *ss = llvm::dyn_cast<StructStmt>(stmt)) {
            std::vector<llvm::Type *> fieldsTypes(ss->GetBody().size());
            std::unordered_map<std::string, Field> fields;
            llvm::StructType *structType = llvm::StructType::create(_context, ss->GetName());
            Struct s { .Name = ss->GetName(), .Type = structType, .Fields = fields, .TraitsImplements = {} };
            _structs.emplace(ss->GetName(), s);
            for (int i = 0; i < fieldsTypes.size(); ++i) {
                VarDeclStmt *vds = llvm::dyn_cast<VarDeclStmt>(ss->GetBody()[i]);
                fieldsTypes[i] = typeToLLVM(vds->GetType());
                fields.emplace(vds->GetName(), Field { .Name = vds->GetName(), .Type = typeToLLVM(vds->GetType()), .ASTType = vds->GetType(),
                                                       .Val = vds->GetExpr() ? Visit(vds->GetExpr()) : nullptr, .ManualInitialized = false,
                                                       .Index = i });

                _structs.at(ss->GetName()).Fields.emplace(vds->GetName(),
                            Field { .Name = vds->GetName(), .Type = fieldsTypes[i], .ASTType = vds->GetType(),
                                    .Val = vds->GetExpr() ? Visit(vds->GetExpr()) : nullptr, .ManualInitialized = false, .Index = i });
            }
            structType->setBody(fieldsTypes);
        }
        // новый код
        else if (TraitDeclStmt *tds = llvm::dyn_cast<TraitDeclStmt>(stmt)) {
            Trait t { .Name = tds->GetName(), .Methods = {} };
            for (auto method : tds->GetBody()) {
                if (FunDeclStmt *fds = llvm::dyn_cast<FunDeclStmt>(method)) {
                    std::vector<llvm::Type *> args(fds->GetArgs().size());
                    for (int i = 0; i < args.size(); ++i) {
                        args[i] = typeToLLVM(fds->GetArgs()[i].GetType());
                    }
                    t.Methods.push_back({ fds->GetName(),
                                          Method { .Name = fds->GetName(), .RetType = typeToLLVM(fds->GetRetType()), .Args = args } });
                }
            }
            _traits.emplace(tds->GetName(), t);
        }
    }
}
VisitFunCallExpr
llvm::Value *
CodeGen::VisitFunCallExpr(FunCallExpr *fce) {
    llvm::Function *fun = _functions.at(fce->GetName());
    std::vector<llvm::Value *> args(fce->GetArgs().size());
    for (int i = 0; i < fce->GetArgs().size(); ++i) {
        llvm::Value *val = Visit(fce->GetArgs()[i]);
        llvm::Type *expectedType = fun->getFunctionType()->getParamType(i);

        if (expectedType->isStructTy() && expectedType->getStructNumElements() == 2 && 
            !val->getType()->isPointerTy()) {

            std::string structName = resolveStructName(fce->GetArgs()[i]);
            std::string traitName = _funArgsTypes.at(fce->GetName())[i].GetVal();

            llvm::Value *fatPtr = llvm::UndefValue::get(expectedType);

            if (!val->getType()->isPointerTy()) {
                llvm::AllocaInst *tmp = _builder.CreateAlloca(val->getType());
                _builder.CreateStore(val, tmp);
                val = tmp;
            }
            llvm::Value *dataPtr = _builder.CreateBitCast(val, llvm::PointerType::get(_context, 0));
            fatPtr = _builder.CreateInsertValue(fatPtr, dataPtr, 0);

            llvm::Value *vtable = getOrCreateVTable(structName, traitName);
            llvm::Value *vtablePtr = _builder.CreateBitCast(vtable, llvm::PointerType::get(llvm::PointerType::get(_context, 0), 0));
            fatPtr = _builder.CreateInsertValue(fatPtr, vtablePtr, 1);

            args[i] = fatPtr;
        }
        else {
            args[i] = implicitlyCast(val, expectedType);
        }
    }
    return _builder.CreateCall(fun, args, fun->getName() + ".call");
}
VisitMethodCallExpr
llvm::Value *
CodeGen::VisitMethodCallExpr(MethodCallExpr *mce) {
    bool oldLoad = createLoad;
    createLoad = mce->GetObjType().IsPointer();
    llvm::Value *obj = Visit(mce->GetObject());
    createLoad = oldLoad;

    ASTType objType = mce->GetObjType();
    if (objType.IsPointer()) {
        for (int i = 0; i < objType.GetPointerDepth() - 1; ++i) {
            createCheckForNil(obj, mce->GetStartLoc());
            obj = _builder.CreateLoad(_builder.getPtrTy(), obj, "ptr.deref");
        }
        createCheckForNil(obj, mce->GetStartLoc());
    }
    else if (objType.GetTypeKind() == ASTTypeKind::Struct) {
        if (!obj->getType()->isPointerTy()) {
            llvm::AllocaInst *tmp = _builder.CreateAlloca(obj->getType());
            _builder.CreateStore(obj, tmp);
            obj = tmp;
        }
    }
    std::string typeName = objType.GetVal();
    if (objType.GetTypeKind() == ASTTypeKind::Trait) {
        llvm::Value *fatPtr = obj;
        if (obj->getType()->isPointerTy() && !llvm::isa<llvm::Argument>(obj)) {
            fatPtr = _builder.CreateLoad(typeToLLVM(objType), obj, "trait.ptr");
        }

        llvm::Value *thisPtr = _builder.CreateExtractValue(fatPtr, 0, "trait.this");
        llvm::Value *vtablePtr = _builder.CreateExtractValue(fatPtr, 1, "trait.vtable");

        Trait &t = _traits.at(typeName);
        int methodIdx = -1;
        for (int i = 0; i < t.Methods.size(); ++i) {
            if (t.Methods[i].first == mce->GetName()) {
                methodIdx = i;
                break;
            }
        }

        llvm::Type *ptrTy = _builder.getPtrTy();
        llvm::Value *gep = _builder.CreateConstGEP1_32(ptrTy, vtablePtr, methodIdx);
        llvm::Value *funcPtr = _builder.CreateLoad(ptrTy, gep, "vfunc.addr");

        std::vector<llvm::Value *> args = { thisPtr };
        for (auto *arg : mce->GetArgs()) {
            args.push_back(Visit(arg));
        }

        auto &m = t.Methods[methodIdx].second;
        llvm::FunctionType *FTy = llvm::FunctionType::get(m.RetType, m.Args, false);
        return _builder.CreateCall(FTy, funcPtr, args);
    }
    
    std::string structName = resolveStructName(mce->GetObject());
    if (structName.empty() && obj->getType()->isStructTy()) {
        structName = obj->getType()->getStructName().str();
        size_t dotPos = structName.find('.');
        if (dotPos != std::string::npos) {
            structName = structName.substr(0, dotPos);
        }
    }

    if (_traits.count(structName)) {
        llvm::Value *thisPtr = _builder.CreateExtractValue(obj, 0, "trait.this");
        llvm::Value *vtablePtr = _builder.CreateExtractValue(obj, 1, "trait.vtable");

        Trait &t = _traits.at(structName);
        int methodIndex = -1;
        llvm::Type* retType = nullptr;

        for (int i = 0; i < t.Methods.size(); ++i) {
            if (t.Methods[i].first == mce->GetName()) {
                methodIndex = i;
                retType = t.Methods[i].second.RetType;
                break;
            }
        }

        if (methodIndex == -1) {
            return nullptr;
        }

        llvm::Type *voidPtrTy = llvm::PointerType::get(_context, 0);
        llvm::Value *gep = _builder.CreateConstGEP1_32(voidPtrTy, vtablePtr, methodIndex);
        llvm::Value *funRaw = _builder.CreateLoad(voidPtrTy, gep, "vfunc.ptr");

        std::vector<llvm::Value *> args(mce->GetArgs().size() + 1);
        args[0] = thisPtr;
        for (int i = 0; i < mce->GetArgs().size(); ++i) {
            args[i + 1] = implicitlyCast(Visit(mce->GetArgs()[i]), t.Methods[methodIndex].second.Args[i]);
        }
        llvm::FunctionType *method = llvm::FunctionType::get(retType, t.Methods[methodIndex].second.Args, false);

        return _builder.CreateCall(method, funRaw, args);
    }

    std::string fullName = structName + "." + mce->GetName();
    if (_functions.find(fullName) == _functions.end()) {
        return nullptr;
    }

    llvm::Function *fun = _functions.at(fullName);
    std::vector<llvm::Value *> args(fun->getFunctionType()->getNumParams());

    if (!obj->getType()->isPointerTy()) {
        llvm::AllocaInst *alloca = _builder.CreateAlloca(obj->getType());
        _builder.CreateStore(obj, alloca);
        obj = alloca;
    }
    args[0] = obj;

    const std::vector<ASTType> &paramASTTypes = _funArgsTypes.at(fullName);
    for (int i = 0; i < mce->GetArgs().size(); ++i) {
        llvm::Value *val = Visit(mce->GetArgs()[i]);
        llvm::Type *expectedType = fun->getFunctionType()->getParamType(i + 1);

        if (paramASTTypes[i + 1].GetTypeKind() == ASTTypeKind::Trait) {
            std::string structName = resolveStructName(mce->GetArgs()[i]);
            std::string traitName = _funArgsTypes.at(fullName)[i + 1].GetVal();

            llvm::Value *fatPtr = llvm::UndefValue::get(expectedType);

            if (!val->getType()->isPointerTy()) {
                llvm::AllocaInst *tmp = _builder.CreateAlloca(val->getType());
                _builder.CreateStore(val, tmp);
                val = tmp;
            }
            llvm::Value *dataPtr = _builder.CreateBitCast(val, llvm::PointerType::get(_context, 0));
            fatPtr = _builder.CreateInsertValue(fatPtr, dataPtr, 0);

            llvm::Value *vtable = getOrCreateVTable(structName, traitName);
            llvm::Value *vtablePtr = _builder.CreateBitCast(vtable, llvm::PointerType::get(llvm::PointerType::get(_context, 0), 0));
            fatPtr = _builder.CreateInsertValue(fatPtr, vtablePtr, 1);

            args[i + 1] = fatPtr;
        }
        else {
            args[i + 1] = implicitlyCast(val, expectedType);
        }
    }
    return _builder.CreateCall(fun, args);
}

В /

implicitlyCast
llvm::Value *
CodeGen::implicitlyCast(llvm::Value *src, llvm::Type *expectType) {
    // старый код
    if (expectType->isStructTy() && expectType->getStructNumElements() == 2) {
        if (src->getType() == expectType) {
            return src;
        }

        std::string traitName = expectType->getStructName().str();
        size_t dotPos = traitName.find('.');
        if (dotPos != std::string::npos) {
            traitName = traitName.substr(0, dotPos);
        }

        if (_traits.count(traitName)) {
            llvm::Value* fatPtr = llvm::UndefValue::get(expectType);

            llvm::Value *dataPtr;
            if (src->getType()->isPointerTy()) {
                dataPtr = _builder.CreateBitCast(src, llvm::PointerType::get(_context, 0));
            }
            else {
                llvm::AllocaInst *tmp = _builder.CreateAlloca(src->getType());
                _builder.CreateStore(src, tmp);
                dataPtr = _builder.CreateBitCast(tmp, llvm::PointerType::get(_context, 0));
            }
            fatPtr = _builder.CreateInsertValue(fatPtr, dataPtr, 0);
            llvm::Value *vtable = getOrCreateVTable(src->getType()->getStructName().str(), traitName);
            llvm::Value *vtablePtr = _builder.CreateBitCast(vtable, llvm::PointerType::get(llvm::PointerType::get(_context, 0), 0));
            fatPtr = _builder.CreateInsertValue(fatPtr, vtablePtr, 1);
            return fatPtr;
        }
    }
    // старый код
}
typeToLLVM
llvm::Type *
CodeGen::typeToLLVM(ASTType type) {
    llvm::Type *base;
    switch (type.GetTypeKind()) {
        // старый код
        case ASTTypeKind::Trait: {
            llvm::StructType *existingType = llvm::StructType::getTypeByName(_context, type.GetVal());
            if (existingType) {
                return existingType;
            }
            llvm::Type *voidPtrTy = llvm::PointerType::get(_context, 0);
            llvm::Type *vtablePtrTy = llvm::PointerType::get(voidPtrTy, 0);
            return llvm::StructType::create(_context, { voidPtrTy, vtablePtrTy }, type.GetVal());
        }
        // старый код
    }
    // тут было нечто для указателей, но об этом позже :)
    return base;
}
getOrCreateVTable
llvm::Value *
CodeGen::getOrCreateVTable(const std::string &structName, const std::string &traitName) {
    std::string vtableName = "vtable." + structName + ".as." + traitName;
    if (auto *existingVTable = _module->getNamedGlobal(vtableName)) {
        return existingVTable;
    }

    Trait &t = _traits.at(traitName);
    std::vector<llvm::Constant *> functions(t.Methods.size());
    llvm::Type *voidPtrTy = llvm::PointerType::get(_context, 0);

    int i = 0;
    for (const auto &[methodName, _] : t.Methods) {
        std::string fullMethodName = structName + "." + methodName;
        llvm::Function *fun = _functions.at(fullMethodName);
        functions[i] = llvm::ConstantExpr::getBitCast(fun, voidPtrTy);
        ++i;
    }

    llvm::ArrayType *vtableArrTy = llvm::ArrayType::get(voidPtrTy, functions.size());
    llvm::GlobalVariable *vtable = new llvm::GlobalVariable(*_module, vtableArrTy, true, llvm::GlobalValue::InternalLinkage,
                                                            llvm::ConstantArray::get(vtableArrTy, functions), vtableName);
    return vtable;
}
castToTrait
llvm::Value *
CodeGen::castToTrait(llvm::Value *src, llvm::Type *traitType, const std::string &structName) {
    llvm::Value *fatPtr = llvm::UndefValue::get(traitType);
    
    llvm::Value *dataPtr;
    if (src->getType()->isPointerTy()) {
        dataPtr = _builder.CreateBitCast(src, _builder.getPtrTy());
    }
    else {
        llvm::AllocaInst *tmp = _builder.CreateAlloca(src->getType());
        _builder.CreateStore(src, tmp);
        dataPtr = tmp;
    }
    fatPtr = _builder.CreateInsertValue(fatPtr, dataPtr, 0);

    std::string traitName = traitType->getStructName().str();
    llvm::Value *vtable = getOrCreateVTable(structName, traitName);
    llvm::Value *vtablePtr = _builder.CreateBitCast(vtable, _builder.getPtrTy());
    fatPtr = _builder.CreateInsertValue(fatPtr, vtablePtr, 1);
    return fatPtr;
}
VisitVarDeclStmt
llvm::Value *
CodeGen::VisitVarDeclStmt(VarDeclStmt *vds) {
    llvm::Type *type = typeToLLVM(vds->GetType());
    llvm::Value *initializer = nullptr;
    std::string structName;
    bool isTraitType = vds->GetType().GetTypeKind() == ASTTypeKind::Trait;
    if (vds->GetExpr()) {
        initializer = Visit(vds->GetExpr());
        // новый код
        if (isTraitType) {
            std::string concreteStruct = resolveStructName(vds->GetExpr());
            initializer = castToTrait(initializer, type, concreteStruct);
        }
        else {
            type = typeToLLVM(vds->GetType());
            initializer = implicitlyCast(initializer, typeToLLVM(vds->GetType()));
        }
    }
    else {
        // новый код
        if (isTraitType) {
            initializer = llvm::ConstantAggregateZero::get(llvm::cast<llvm::StructType>(type));
        }
        else if (vds->GetType().GetTypeKind() != ASTTypeKind::Struct) {
            initializer = llvm::Constant::getNullValue(typeToLLVM(vds->GetType()));
        }
        else {
            initializer = defaultStructConst(vds->GetType());
        }
    }
    llvm::Value *var;
    // ...
    if (vds->GetType().GetTypeKind() == ASTTypeKind::Struct ||
        vds->GetType().GetTypeKind() == ASTTypeKind::Trait) {
        llvm::MDNode *metadata = llvm::MDNode::get(_context, llvm::MDString::get(_context, vds->GetType().GetVal()));
        if (_vars.size() == 1) {
            llvm::cast<llvm::GlobalVariable>(var)->setMetadata("struct_name", metadata);
        }
        else {
            llvm::cast<llvm::AllocaInst>(var)->setMetadata("struct_name", metadata);
        }
    }
    // ...
}
VisitVarAsgnStmt
llvm::Value *
CodeGen::VisitVarAsgnStmt(VarAsgnStmt *vas) {
    llvm::Value *val = Visit(vas->GetExpr());
    auto varsCopy = _vars;
    while (!varsCopy.empty()) {
        if (auto var = varsCopy.top().find(vas->GetName()); var != varsCopy.top().end()) {
            auto &[varVal, llvmType, type] = var->second;
            // ...

            // новый код
            if (type.GetTypeKind() == ASTTypeKind::Trait) {
                std::string concreteStructName = resolveStructName(vas->GetExpr());
                llvm::Type *traitLLVMTy = llvmType;
                val = castToTrait(val, traitLLVMTy, concreteStructName);
            }
            
            ASTType finalType = type;
            val = implicitlyCast(val, typeToLLVM(finalType));
            _builder.CreateStore(val, targetAddr);
            return nullptr;
        }
        varsCopy.pop();
    }
    return nullptr;
}

Как же много изменений! Сейчас всё объясню. Будем идти от простого к сложному, начнем с DeclareFunctionsAndStructures. Мы создаем тип трейта, обрабатываем все методы и сохраняем этот новый трейт в мапу с трейтами _traits. Всё. В typeToLLVM добавляем обработку трейта. Смысл в том, чтобы создать ту самую структуру-обёртку. Если она существует - возвращаем существующую, иначае создаем новую. Для создания (или получения уже существующей) таблицы виртуальных методов (vtable) нужен новый метод getOrCreateVTable. Его задача создать vtable для конкретной структуры для конкретного трейта. Так как все методы из имплементаций уже созданы после вызова DeclareFunctionsAndStructures, то можно спокойно брать уже существующие методы из мапы функций и кастить их к void *. В итоге мы получаем новую (или уже существующую) vtable с нужными указателями на функции. castToTrait приводит значение llvm::Value * к структуре-обертке (трейту). Как раз этот метод будет использоваться в VisitVarDeclStmt, если вдруг мы решим определить переменную с типом трейта + проинициализируем. Однако если тип переменной - трейт, но инициализатора нет, то просто зануляем значение. castToTrait также понадобится и в VisitVarAsgnStmt, если мы захотим изменить значение переменной с типом трейта. В implicitlyCast мы поступим довольно хитро: мы не можем знать, является ли expectType трейтом, поэтому будем смотреть на его сигнатуру - структура с двумя полями. Однако если структуры равны, то просто вернем исходную. Затем получаем имя трейта из имени структуры expectType и если трейт с таким именем действительно существует, то делаем примерно то же самое, что и в castToTrait. В VisitFunCallExpr проворачиваем примерно то же самое с аргументами.

Всё это позволяет нам получить готовые трейты. Не знаю как вы, но я был очень доволен этим результатом!

Указатели

Казалось бы, простая вещь, однако сколько хлопот было с этими аллокациями и сегфолтами...

Ключевая проблема указателей в LLVM в том, что с версии LLVM 15 и выше указатели больше не хранят базовый тип (теперь они называются непрозрачными) и один хрен ты получишь базовый тип указателя. Приходится придумывать костыли в виде сохранений ASTType, чтобы просто понимать глубину указателя и его базовый тип. Помимо этих проблем ещё появилась такая проблема, что иногда нам необходимо создать _builder.CreateLoad(...), а иногда нам нужен просто адрес (alloca). Для того, чтобы учесть все случаи, был введён костыль в виде флага createLoad, который и показывает, будет производиться загрузка или будет получен адрес. Также при доступе к полю/методу с указателя на структуру должно производиться неявное разыменование. С this вообще отдельная история - он не как все, и при использовании this в выражении он должен вести себя не как указатель на структуру, а как экземпляр структуры. Однако если менять поля в this, то он будет вести себя как указатель. Правда, если менять сам this (this = Struct { ... }), то он снова ведет себя как экземпляр, хотя и изменения вступят в силу. Это всё для того, чтобы обеспечить более приятный опыт использования языком. Также есть некоторая особенная проблема с аргументами в функциях. Видите-ли, аргументы в функциях это не аллокации, а уже готовые значения, а значит просто так применить к ним _builder.Store(...) не получится - придется создавать временную аллокацию. С этим багажом знаний и предупреждений можно начинать работу. Изменённые структуры для описания AST смотрите в сурсах.

Семантика

VisitNilExpr + VisitDerefExpr + VisitRefExpr
std::optional<ASTVal>
SemanticAnalyzer::VisitNilExpr(NilExpr *ne) {
    return ASTVal(ASTType(ASTTypeKind::Nil, "nil", false, 0), ASTValData { .i32Val = 0 }, true);
}

std::optional<ASTVal>
SemanticAnalyzer::VisitDerefExpr(DerefExpr *de) {
    std::optional<ASTVal> val = Visit(de->GetExpr());
    if (!val->GetType().IsPointer()) {
        _diag.Report(de->GetExpr()->GetStartLoc(), ErrDerefFromNonPtr)
            << llvm::SMRange(de->GetStartLoc(), de->GetEndLoc());
        return val;
    }
    if (val->IsNil()) {
        _diag.Report(de->GetExpr()->GetStartLoc(), ErrDerefFromNil)
            << llvm::SMRange(de->GetStartLoc(), de->GetEndLoc());
        return val;
    }
    de->SetExprType(val->GetType());
    return ASTVal(val->GetType().Deref(), val->GetData(), false);
}

std::optional<ASTVal>
SemanticAnalyzer::VisitRefExpr(RefExpr *re) {
    std::optional<ASTVal> val = Visit(re->GetExpr());
    if (re->GetExpr()->GetKind() == NkVarExpr) {
        return ASTVal(val->GetType().Ref(), val->GetData(), false);
    }
    if (FieldAccessExpr *fae = llvm::dyn_cast<FieldAccessExpr>(re->GetExpr())) {
        if (fae->GetObject()->GetKind() == NkVarExpr) {
            return ASTVal(val->GetType().Ref(), val->GetData(), false);
        }
    }
    _diag.Report(re->GetExpr()->GetStartLoc(), ErrRefFromRVal)
        << llvm::SMRange(re->GetStartLoc(), re->GetEndLoc());
    return ASTVal(ASTType(ASTTypeKind::I32, "i32", false, 0), ASTValData { .i32Val = 0 }, false);
}
VisitVarAsgnStmt
std::optional<ASTVal>
SemanticAnalyzer::VisitVarAsgnStmt(VarAsgnStmt *vas) {
    // старый код
    while (!varsCopy.empty()) {
        if (auto var = varsCopy.top().find(vas->GetName()); var != varsCopy.top().end()) {
            // старый код
            ASTVal val = Visit(vas->GetExpr()).value_or(ASTVal::GetDefaultByType(ASTType::GetNothType()));
            ASTType type = var->second.Type;
            // новый код
            if (var->second.Type.IsPointer()) {
                for (unsigned char dd = vas->GetDerefDepth(); dd > 0; type.Deref(), --dd) {
                    if (!type.IsPointer()) {
                        _diag.Report(vas->GetStartLoc(), ErrDerefFromNonPtr)
                            << llvm::SMRange(vas->GetStartLoc(),
                                             llvm::SMLoc::getFromPointer(vas->GetStartLoc().getPointer() + vas->GetDerefDepth() +
                                                                         vas->GetName().length()));
                        break;
                    }
                    if (val.IsNil()) {
                        _diag.Report(vas->GetExpr()->GetStartLoc(), ErrDerefFromNil)
                            << llvm::SMRange(vas->GetStartLoc(), vas->GetEndLoc());
                        break;
                    }
                }
            }
            // старый код
        }
        // старый код
    }
    // старый код
    return std::nullopt;
}

Это не единственные изменения в семантике, однако, это самые значимые изменения. VisitNilExpr самый простой - просто создает значение nil. VisitDerefExpr во первых проверяет, является ли выражение вообще указателем, а затем проверяет значение выражения на nil. Если оно nil - семантическая ошибка разыменования. Однако семантика не всегда сможет понять, когда указатель равен nil, поэтому в кодгене нам придется также создавать рантайм проверки, но об этом позже. VisitRefExpr создает указатель из выражения. Однако далеко не каждое выражение способно вернуть адрес. Адрес могут вернуть только объекты со стека (переменные), поэтому нужно отсеять все остальные возможные выражения. У полей тоже можно взять адрес, но лишь в том случае, если мы получили поле из структуры напрямую. То есть:

struct Human {
    pub var age: i32;
}

fun main(): i32 {
    var h: Human = Human { age: 16 };
    var aP: *i32 = &h.age;
    return 0;
}

Сработает, однако:

struct Human {
    pub var age: i32;
}

fun newHuman(age: i32): Human {
    return Human { age: age };
}

fun main(): i32 {
    var aP: *i32 = &newHuman(16).age;
    return 0;
}

Уже не прокатит, потому что значение было взято из функции. То же самое происходит и с методами (&method().field).

В VisitVarAsgnStmt может существовать вариант записи *a = 10 или типа того. Для этого мы разыменовываем переменную столько раз, сколько звездочек мы указали перед ней. Естественно есть случаи, когда количество разыменований превышает глубину указателя и нам нужно проверять это. Также может быть и разыменование nil, которые надо проверять.

Кодген

typeToLLVM
llvm::Type *
CodeGen::typeToLLVM(ASTType type) {
    llvm::Type *base;
    // большой switch
    for (unsigned char pd = type.GetPointerDepth(); pd > 0; --pd) {
        base = llvm::PointerType::get(base, 0);
    }
    return base;
}
createCheckForNil
void
CodeGen::createCheckForNil(llvm::Value *ptr, llvm::SMLoc loc) {
    auto [line, col] = _srcMgr.getLineAndColumn(loc);

    std::string msgStr = "Error: Null pointer dereference at " + _module->getSourceFileName() + ":" +
                         std::to_string(line) + ":" + std::to_string(col) + "!\n";

    llvm::BasicBlock *currentBB = _builder.GetInsertBlock();
    llvm::Function *parentFun = currentBB->getParent();

    llvm::BasicBlock *notNullBB = llvm::BasicBlock::Create(_context, "not_null", parentFun);
    llvm::BasicBlock *nullBB = llvm::BasicBlock::Create(_context, "null_error", parentFun);

    llvm::Value *isNull = _builder.CreateIsNull(ptr, "is_null_check");

    _builder.CreateCondBr(isNull, nullBB, notNullBB);
    _builder.SetInsertPoint(nullBB);

    llvm::Constant *errMsg = llvm::ConstantDataArray::getString(_context, msgStr, true);
    llvm::GlobalVariable *errMsgGlobal = new llvm::GlobalVariable(*_module, errMsg->getType(), true, llvm::GlobalValue::PrivateLinkage, errMsg, "null_err_msg");
    errMsgGlobal->setUnnamedAddr(llvm::GlobalValue::UnnamedAddr::Global);
    errMsgGlobal->setAlignment(llvm::MaybeAlign(1));

    llvm::Value *errMsgPtr = _builder.CreateGEP(errMsgGlobal->getValueType(), errMsgGlobal, { _builder.getInt64(0), _builder.getInt32(0) }, "err_msg_ptr");

    _builder.CreateCall(_module->getFunction("printf"), { errMsgPtr }, "printf_call");

    _builder.CreateCall(_module->getFunction("abort"));
    _builder.CreateUnreachable();

    _builder.SetInsertPoint(notNullBB);
}
VisitBinaryExpr
llvm::Value *
CodeGen::VisitBinaryExpr(BinaryExpr *be) {
    llvm::Value *lhs = Visit(be->GetLHS());
    llvm::Value *rhs = Visit(be->GetRHS());
    llvm::Type *lhsType = lhs->getType();
    llvm::Type *rhsType = rhs->getType();

    // новый код
    bool leftIsPtr = lhsType->isPointerTy();
    bool rightIsPtr = rhsType->isPointerTy();
    int numPointers = leftIsPtr + rightIsPtr;
    if (numPointers == 1 && (be->GetOp().GetKind() == TkPlus || be->GetOp().GetKind() == TkMinus)) {
        llvm::Value *ptrVal = leftIsPtr ? lhs : rhs;
        llvm::Value *intVal = leftIsPtr ? rhs : lhs;
        intVal = implicitlyCast(intVal, _builder.getInt64Ty());
        bool subtract = be->GetOp().GetKind() == TkMinus;
        if (subtract) {
            intVal = _builder.CreateNeg(intVal, "neg.offset");
        }
        ASTType resultASTType = ASTType::GetCommon(be->GetLHSType(), be->GetRHSType()).Deref();
        llvm::Type *pointeeLLVMTy = typeToLLVM(resultASTType);
        llvm::Value *gep = _builder.CreateInBoundsGEP(pointeeLLVMTy, ptrVal, {intVal}, "ptr.arith");
        return gep;
    }
    else if (numPointers == 2 && (be->GetOp().GetKind() == TkPlus || be->GetOp().GetKind() == TkMinus)) {
        ASTType leftASTType = be->GetLHSType();
        llvm::Type *pointeeLLVMTy = typeToLLVM(leftASTType.Deref());
        llvm::Value *leftInt = _builder.CreatePtrToInt(lhs, _builder.getInt64Ty());
        llvm::Value *rightInt = _builder.CreatePtrToInt(rhs, _builder.getInt64Ty());
        llvm::Value *diff = _builder.CreateSub(leftInt, rightInt, "ptr.diff.bytes");
        const llvm::DataLayout &dl = _module->getDataLayout();
        uint64_t elemSize = dl.getTypeAllocSize(pointeeLLVMTy);
        if (elemSize > 1) {
            llvm::Value *sizeVal = _builder.getInt64(elemSize);
            diff = _builder.CreateExactSDiv(diff, sizeVal, "ptr.diff.elements");
        }
        ASTType resultASTType = ASTType::GetCommon(be->GetLHSType(), be->GetRHSType()).Deref();
        return implicitlyCast(diff, typeToLLVM(resultASTType));
    }

    llvm::Type *commonType = getCommonType(lhsType, rhsType);
    if (lhsType != commonType) {
        lhs = implicitlyCast(lhs, commonType);
        lhsType = lhs->getType();
    }
    if (rhsType != commonType) {
        rhs = implicitlyCast(rhs, commonType);
        rhsType = rhs->getType();
    }
    // ...
}
VisitNilExpr + VisitDerefExpr + VisitRefExpr
llvm::Value *
CodeGen::VisitNilExpr(NilExpr *ne) {
    return llvm::ConstantPointerNull::get(llvm::PointerType::get(_context, 0));
}

llvm::Value *
CodeGen::VisitDerefExpr(DerefExpr *de) {
    bool oldLoad = createLoad;
    createLoad = true;
    llvm::Value *ptrVal = Visit(de->GetExpr());
    createLoad = oldLoad;
    if (!ptrVal->getType()->isPointerTy()) {
        return ptrVal;
    }
    if (_vars.size() != 1) {
        createCheckForNil(ptrVal, de->GetStartLoc());
    }
    if (createLoad) {
        ptrVal = _builder.CreateLoad(typeToLLVM(de->GetExprType().Deref()),
                                     ptrVal, "deref.load");
    }
    return ptrVal;
}

llvm::Value *
CodeGen::VisitRefExpr(RefExpr *re) {
    bool oldLoad = createLoad;
    createLoad = false;
    llvm::Value *ptr = Visit(re->GetExpr());
    createLoad = oldLoad;
    return ptr;
}
VisitVarAsgnStmt
llvm::Value *
CodeGen::VisitVarAsgnStmt(VarAsgnStmt *vas) {
    llvm::Value *val = Visit(vas->GetExpr());
    auto varsCopy = _vars;
    while (!varsCopy.empty()) {
        if (auto var = varsCopy.top().find(vas->GetName()); var != varsCopy.top().end()) {
            auto &[varVal, llvmType, type] = var->second;
            if (auto arg = llvm::dyn_cast<llvm::Argument>(varVal)) {
                if (arg->getType()->isPointerTy()) {
                    _builder.CreateStore(val, arg);
                    return nullptr;
                }
                llvm::AllocaInst *alloca = _builder.CreateAlloca(arg->getType(), nullptr, arg->getName());
                _builder.CreateStore(val, alloca);
                return nullptr;
            }

            llvm::Value *targetAddr = varVal;
            ASTType currentASTType = type;
            if (vas->GetDerefDepth() > 0) {
                if (llvm::isa<llvm::AllocaInst>(targetAddr) || llvm::isa<llvm::GlobalVariable>(targetAddr)) {
                    targetAddr = _builder.CreateLoad(llvmType, targetAddr, vas->GetName() + ".addr");
                }
            }
            for (unsigned char dd = vas->GetDerefDepth(); dd > 1; --dd) {
                createCheckForNil(targetAddr, vas->GetStartLoc());
                targetAddr = _builder.CreateLoad(_builder.getPtrTy(), targetAddr, "deref");
                currentASTType = currentASTType.Deref();
            }
            if (vas->GetDerefDepth() > 0) {
                createCheckForNil(targetAddr, vas->GetStartLoc());
            }
            for (unsigned char dd = vas->GetDerefDepth(); dd > 0; --dd, type.Deref());

            if (type.GetTypeKind() == ASTTypeKind::Trait) {
                std::string concreteStructName = resolveStructName(vas->GetExpr());
                llvm::Type *traitLLVMTy = llvmType;
                val = castToTrait(val, traitLLVMTy, concreteStructName);
            }
            
            ASTType finalType = type;
            val = implicitlyCast(val, typeToLLVM(finalType));
            _builder.CreateStore(val, targetAddr);
            return nullptr;
        }
        varsCopy.pop();
    }
    return nullptr;
}

Также начнем с простого - с typeToLLVM. В свитче мы определяем базовый тип, а после свитча создаем указатели и возвращаем. Для указателей был введён nil (null). Это целое самостоятельное выражение, поэтому ему посвещён VisitNilExpr с невероятно простой логикой в одну строку, которая создает null для непрозрачного указателя LLVM IR. VisitRefExpr необходим для того, чтобы получить адрес (указатель) с объекта на стеке. Для этого мы отключим флаг createLoad, чтобы яне происходила загрузка значения из alloca. Затем просто вернем этот alloca (он и есть указатель). VisitDerefExpr нужен для разыменования указателя. Однако, если указатель хранил nil, то дальнейшие действия после разыменования приведут к segmentation fault (разыменование null). Для этого создаем рантайм проверку на nil (createCheckForNil). createCheckForNil работает только в локальной области видимости, так как работает так (эквивален на си):

if (ptr == NULL) {
    printf("Error: Null pointer dereference at ...\n");
    abort();
}

А поскольку в глобальной области не могут существовать условные операторы или вызовы функций, то и проверок на nil там быть не может (в будущем планирую попробовать исправить ситуацию). Сейчас при разыменовании ук��зателя в глобальной области может произойти segmentation fault на уровне компилятора из-за createCheckForNil, будьте осторожны! К слову, если вдруг ошибка произошла, то вместе с текстом ошибки выведутся координаты этого разыменования в файле.

Код в VisitBinaryExpr выглядит большим, но на самом деле это просто арифметика указателей (ptr - ptr, ptr +- int). ptr + ptr блокируются на уровне семантики.

VisitVarAsgnStmt был переработан полностью. Если по простому: берём адрес объекта, разыменовываем столько раз, сколько звезд написал программист (параллельно генерим рантайм проверки на nil), загружаем значение. Но к чему такая интересная логика для аргументов? А давайте вспомним, что из себя представляет аргумент в LLVM IR? Правильно, аргумент это уже готовое загруженное значение. Если аргумент - указатель, то просто делаем загрузку значения в него как ни в чем не бывало. Однако в противном случае создаем временную аллокацию и уже потом грузим в неё значение.

Вывод

Указатели и трейты - мощный апдейт языка. Надеюсь, эта статья (как и мои предыдущие) помогут вам писать свои компиляторы. Напоминаю про просьбу в начале статьи. Всем удачи :)