В предыдущей статье мы закончили на том, что мы написали лексический и синтаксический анализаторы для нашего учебного языка. В данной статье мы продолжим начатое и рассмотрим следующую стадию анализа исходного кода программы — семантический анализ. Лексический и синтаксический анализ Семантический анализ Генерация кода Поддержка составных типов Поддержка классов и перегрузки функций Основная задача семантического анализа заключается в проверки того, что программа корректна с точки зрения языка, например: Все переменные в программе объявлены; Все выражения совершаются над корректными типами; Если в программе используется 	безусловный/условный переход, то метка, на которую совершается переход должна существовать; Функция возвращает значение корректного типа. Замечание. Нужно понимать, что семантический анализ не может выловить все ошибки в программе, а только те их типы, которые были заложены разработчиком компилятора или описаны в спецификации языка. Например в языке C++ компилятор не сможет обнаружить все ошибки работы с памятью, даже если разработчик компилятора внес специальную обработку для выявления ошибок такого типа. Но в Rust ошибки такого типа исключаются на уровне самого языка. Все семантические правила обычно описываются в спецификации конкретного языка и описывают критерии нахождения не корректных программ на данном языке. Область видимости — это часть программы, в пределах которой идентификатор, объявленный как имя некоторой программной сущности (обычно — переменной, типа данных или функции), остаётся связанным с этой сущностью, то есть позволяет посредством себя обратиться к ней. Часто в спецификации языка описаны правила, для формирования областей видимости, например может быть глобальная область видимости, область видимости класса, область видимости функции и др. Обычно области видимости образуют иерархию и для поиска сущности, на которую ссылается идентификатор в конкретном месте программы, может понадобиться поиск этого идентификатора во всех уровнях этой иерархии. В simple есть следующие виды областей видимости: Модуль (все объявления объявленные на самом верхнем уровне файла); Класс/Структура (более подробно будут рассмотрены в последующих частях серии); Функция; Блок в функции. Так же в simple объявления не могут перекрывать другие объявление доступные в данной области видимости (за исключением перегрузки функций (рассмотри в последующих частях серии)). В simple, любые переменная объявленная в функции может быть использована в любом месте после ее объявления, любое имя объявленное в модуле может быть использовано в любом месте в этом модуле (т. е. разрешено использовать имя класса до его объявления, если оно находится в этом же модуле). Более подробно про области видимости можно почитать в [1]. Для работы с областью видимости будем использовать следующий класс: Ниже приведу реализацию данного класса:  Суть проверки типов заключается в том, что бы проверить, что все операции выполняемые в программе производятся с операндами корректных типов. Проверка типов бывает: Статическая (данная проверка происходит на этапе компиляции программы); Динамическая (данная проверка происходит на этапе выполнения программы). Более подробно можно почитать в [2]. В simple у нас будет статическая проверка и все типы будут определяться во время анализа исходного кода программы. В последующих частях мы добавим классы с поддержкой виртуальных функций, при вызове которых результирующая функция будет разрешаться во время выполнения программы. Для начала рассмотрим какие именно функции члены иерархии типов будут отвечать за семантический анализ этих ветвей AST: Если посмотреть на приведенный выше код, то можно увидеть, что функция semantic возвращает TypeAST*, это нужно для того, что бы при необходимости можно было бы вернуть новый тип в замен старого. Например в simple можно объявить переменную с типом A и на момент построения дерева мы не можем сказать, какой именно тип будет иметь данная переменная, поэтому во время парсинга будет создан экземпляр типа QualifiedTypeAST, который во время семантического анализа будет заменен типом StructTypeAST или ClassTypeAST (все эти типы будут рассмотрены в последующих частях серии). Так же можно увидеть, что существуют функции для создания декорированного имени, данная функция генерирует уникальное имя типа в системе и если два объекта производные от TypeAST имеют одинаковое декорированное имя, то эти типы совпадают. Так же декорация имен нужна для создания уникальных имен функций, например функция после декорации будет иметь следующие имя Более подробнее про декорирование имен (Name mangling) можно посмотреть тут [3]. Теперь мы можем рассмотреть реализацию семантического анализа для иерархии типов:  На данной стадии у нас есть только несколько базовых типов и семантический анализ для иерархии типов достаточно прост, мы просто генерируем декорированные имена для каждого типа. Но в последующих статьях с добавлением более сложных типов семантический анализ усложнится и список преобразований между типами будет так же расширен.   Для начала рассмотрим какие именно функции члены иерархии выражений будут отвечать за семантический анализ этих ветвей AST: Так же, как и у типов, ExprAST после семантического анализа может быть изменено (например можно сделать оптимизацию и произвести вычисление константных выражений, но в нашем случае мы будем использовать это для замены некоторых выражений их аналогами, что бы упростить генерацию кода). Рассмотрим более подробно сам семантический анализ для выражений:  Т.к. все выражения с одним операндом могут быть переписаны в виде эквивалентного выражения с двумя операндами, то для упрощения кодогенерации при семантическом анализе мы проверяем корректность операнда и возвращаем эквивалентный аналог:  Рассмотрим семантический анализ выражений с двумя операндами:  Семантический анализ тернарного оператор:  Семантический анализ вызова функции:  Для начала рассмотрим какие именно функции члены иерархии инструкций будут отвечать за семантический анализ этих ветвей AST: Так же, как и у типов и выражений, StmtAST после семантического анализа может быть изменено (например одна инструкция может быть реализована через другую). Один из важных классов для работы семантического анализа (и генерации кода) является LandingPadAST (более подробное описание зачем он нужен рассмотрим в следующей части серии, на данный момент приведу только те члены класса, которые нужны для семантического анализа): Рассмотрим более подробно сам семантический анализ для инструкций:  Для начала рассмотрим какие именно функции члены иерархии объявлений будут отвечать за семантический анализ этих ветвей AST: Из-за правил поиска идентификаторов в simple, весь семантический анализ был разбит на 5 фаз: Создание списка всех объявлений в области видимости; Разрешение типов базовых классов; Разрешение типов для имен переменных и функций членов структур и классов; Построение таблиц виртуальных функций, конструкторов и деструкторов; Анализ функций и их тел. Разбивка семантики на несколько фаз позволяет упростить грамматику языка, т. к. не нужно вводить дополнительные конструкции для объявления и определения (т. е. введение имени в программу (что бы оно могло быть использовано) и описание ее реализации (т. е. для классов это описание всех его функций и переменных членов, а для функций ее тело). Например в C++ можно сперва объявить класс (написать "class A;"), что позволяет использовать имя этого класса в других объявлениях (например в качестве параметра функции), а потом в другой части исходного кода произвести определение — описать все функции и переменные члены класса. При разбивке семантического анализа на части, такие разграничения объявления и определения на разные сущности просто не нужны, т. к. даже циклические зависимости могут быть спокойно разрешены, т. к. в каждой фазе происходят действия, которые позволят продолжить анализ на следующих стадиях.   Ниже рассмотрим сам семантический анализ для объявлений:  В данной части мы рассмотрели семантический анализ программы на языке simple, произвели проверку ее корректности и подготовили наше промежуточное представление к заключительной части — генерации кода. В статье мы научились изменять изначальное представление программы его аналогом, что позволит упростить многие части генерации кода (например автоматическое создание преобразований типов, где они были необходимы, позволит не задумываться о преобразовании типов на стадии генерации кода, т. к. этим будет заниматься отдельный класс). Автоматическая генерация и модификация дерева может быть полезна при решении многих задач в проектировании своего языка. В последующих частях мы еще не раз будем использовать данный подход, например для генерации кода для вызова конструкторов и деструкторов для объектов класса, генерации конструкторов по умолчанию, а так же автоматический вызов конструкторов и деструкторов для родительских классов. В более сложных языках программирования данный подход может быть применен для генерации замыканий, итераторов и многих других высокоуровневых конструкциях. Разбиение семантического анализа на фазы так же позволяет делать язык более выразительным и дает возможность убрать из него многие ограничения, которые при однопроходном варианте приходилось бы решать другими способами. Полный исходный код доступен на gihub. В следующей статье мы закончим реализацию базовой версии языка simple и добавим генерацию кода для LLVM IR. https://en.wikipedia.org/wiki/Scope_(computer_science) https://en.wikipedia.org/wiki/Type_system https://en.wikipedia.org/wiki/Name_mangling