В предыдущей статье мы закончили на том, что мы написали лексический и синтаксический анализаторы для нашего учебного языка. В данной статье мы продолжим начатое и рассмотрим следующую стадию анализа исходного кода программы — семантический анализ.

Семантический анализ

Основная задача семантического анализа заключается в проверки того, что программа корректна с точки зрения языка, например:

  1. Все переменные в программе объявлены;

  2. Все выражения совершаются над корректными типами;

  3. Если в программе используется безусловный/условный переход, то метка, на которую совершается переход должна существовать;

  4. Функция возвращает значение корректного типа.

Замечание. Нужно понимать, что семантический анализ не может выловить все ошибки в программе, а только те их типы, которые были заложены разработчиком компилятора или описаны в спецификации языка. Например в языке C++ компилятор не сможет обнаружить все ошибки работы с памятью, даже если разработчик компилятора внес специальную обработку для выявления ошибок такого типа. Но в Rust ошибки такого типа исключаются на уровне самого языка.

Все семантические правила обычно описываются в спецификации конкретного языка и описывают критерии нахождения не корректных программ на данном языке.

Область видимости (Scope)

Область видимости — это часть программы, в пределах которой идентификатор, объявленный как имя некоторой программной сущности (обычно — переменной, типа данных или функции), остаётся связанным с этой сущностью, то есть позволяет посредством себя обратиться к ней.

Часто в спецификации языка описаны правила, для формирования областей видимости, например может быть глобальная область видимости, область видимости класса, область видимости функции и др. Обычно области видимости образуют иерархию и для поиска сущности, на которую ссылается идентификатор в конкретном месте программы, может понадобиться поиск этого идентификатора во всех уровнях этой иерархии.

В simple есть следующие виды областей видимости:

  1. Модуль (все объявления объявленные на самом верхнем уровне файла);

  2. Класс/Структура (более подробно будут рассмотрены в последующих частях серии);

  3. Функция;

  4. Блок в функции.

Так же в simple объявления не могут перекрывать другие объявление доступные в данной области видимости (за исключением перегрузки функций (рассмотри в последующих частях серии)). В simple, любые переменная объявленная в функции может быть использована в любом месте после ее объявления, любое имя объявленное в модуле может быть использовано в любом месте в этом модуле (т. е. разрешено использовать имя класса до его объявления, если оно находится в этом же модуле).

Более подробно про области видимости можно почитать в [1].

Для работы с областью видимости будем использовать следующий класс:

struct Scope { 
  Scope(DiagnosticsEngine &D) ;
  Scope(Scope* enclosed) ;
  Scope(ModuleDeclAST* thisModule) ;
 // Поиск идентификатора во всех доступных в данной точке областях видимости
  SymbolAST* find(Name* id); 
  // Поиск идентификатора — члена класса или структуры
  SymbolAST* findMember(Name* id, int flags = 0); 
  // Создать новую область видимости
  Scope* push(); 
  // Создать новую область видимости на основе объявления
  Scope* push(ScopeSymbol* sym); 
  // Закрыть текущую область видимости (возвращает родительский Scope)
  Scope* pop(); 
 // Воссоздать список областей видимости для иерархии классов
  static Scope* recreateScope(Scope* scope, SymbolAST* sym); 
  // Очистка списка областей видимости и оставить только область видимости
  // самого модуля
  static void clearAllButModule(Scope* scope); 
 // Вывод сообщения об ошибке
  template <typename... Args> 
  void report(SMLoc Loc, unsigned DiagID, Args &&...Arguments) { 
    Diag.report(Loc, DiagID, std::forward<Args>(Arguments)...); 
  } 

  Scope* Enclosed; ///< родительский Scope
  ModuleDeclAST* ThisModule; ///< родительский модуль 
  /// символ для текущей области видимости (например функция или класс)
  SymbolAST* CurScope;
  /// функция к которой принадлежит область видимости
  FuncDeclAST* EnclosedFunc;
  StmtAST* BreakLoc; ///< цикл для инструкции break
  StmtAST* ContinueLoc; ///< цикл для инструкции continue 
  LandingPadAST* LandingPad; ///< нужно для генерации кода (см. описание ниже)
  DiagnosticsEngine &Diag; ///< модуль диагностики
};

Ниже приведу реализацию данного класса:

Hidden text
SymbolAST* Scope::find(Name* id) {
  Scope* s = this;

  // Если идентификатор не указан, то это глобальная область 
  // видимости
  if (!id) {
    return ThisModule;
  }

  // Циклический поиск объявления во всех доступных из данной точки
  // областях видимости
  for ( ; s; s = s->Enclosed) {
    if (s->CurScope) {
      SymbolAST* sym = s->CurScope->find(id);

      if (sym) {
        return sym;
      }
    }
  }

  return nullptr;
}

SymbolAST* Scope::findMember(Name* id, int flags) {
  if (CurScope) {
    return CurScope->find(id, flags);
  }

  return nullptr;
}

Scope* Scope::push() {
  Scope* s = new Scope(this);
  return s;
}

Scope* Scope::push(ScopeSymbol* sym) {
  Scope* s = push();
  s->CurScope = sym;
  return s;
}

Scope* Scope::pop() {
  Scope* res = Enclosed;
  delete this;
  return res;
}

Scope* Scope::recreateScope(Scope* scope, SymbolAST* sym) {
  Scope* p = scope;

  // Поиск области видимости самого верхнего уровня (модуля)
  while (p->Enclosed) {
    p = p->Enclosed;
  }

  // Создаем список все родительских объявлений
  SymbolList symList;
  SymbolAST* tmp = sym;

  while (tmp->Parent) {
    symList.push_back(tmp);
    tmp = tmp->Parent;
  }

  // Воссоздаем все области видимости в обратном порядке объявленных 
  // сущностей. Нужно для поиска в имени в иерархии классов
  for (SymbolList::reverse_iterator it = symList.rbegin(), 
       end = symList.rend(); it != end; ++it) {
    p = p->push((ScopeSymbol*)*it);
  }

  // Возвращаем созданный Scope
  return p;
}

void Scope::clearAllButModule(Scope* scope) {
  Scope* p = scope;

  while (p->Enclosed) {
    p = p->pop();
  }
}

Проверка типов

Суть проверки типов заключается в том, что бы проверить, что все операции выполняемые в программе производятся с операндами корректных типов.

Проверка типов бывает:

  1. Статическая (данная проверка происходит на этапе компиляции программы);

  2. Динамическая (данная проверка происходит на этапе выполнения программы).

Более подробно можно почитать в [2].

В simple у нас будет статическая проверка и все типы будут определяться во время анализа исходного кода программы. В последующих частях мы добавим классы с поддержкой виртуальных функций, при вызове которых результирующая функция будет разрешаться во время выполнения программы.

Семантический анализ типов

Для начала рассмотрим какие именно функции члены иерархии типов будут отвечать за семантический анализ этих ветвей AST:

struct TypeAST { 
  ...

  /// Производит семантический анализ для типа
  virtual TypeAST* semantic(Scope* scope) = 0; 
  /// Проверка на то, что данный тип может быть преобразован в "newType"
  virtual bool implicitConvertTo(TypeAST* newType) = 0; 
  /// Проверка на то, что данный тип совпадает с "otherType"
  bool equal(TypeAST* otherType); 
  /// Сгенерировать и поместить в буфер декорированную строку для
  /// данного типа (реализуется в потомках)
  virtual void toMangleBuffer(llvm::raw_ostream& output) = 0; 
  /// Сгенерировать декорированную строку для данного типа
  void calcMangle(); 


  llvm::StringRef MangleName; ///< декорированная строка с именем данного типа
};

Если посмотреть на приведенный выше код, то можно увидеть, что функция semantic возвращает TypeAST*, это нужно для того, что бы при необходимости можно было бы вернуть новый тип в замен старого. Например в simple можно объявить переменную с типом A и на момент построения дерева мы не можем сказать, какой именно тип будет иметь данная переменная, поэтому во время парсинга будет создан экземпляр типа QualifiedTypeAST, который во время семантического анализа будет заменен типом StructTypeAST или ClassTypeAST (все эти типы будут рассмотрены в последующих частях серии).

Так же можно увидеть, что существуют функции для создания декорированного имени, данная функция генерирует уникальное имя типа в системе и если два объекта производные от TypeAST имеют одинаковое декорированное имя, то эти типы совпадают. Так же декорация имен нужна для создания уникальных имен функций, например функция

fn check(s: string, a: float, b: float)

после декорации будет иметь следующие имя

_P5checkPKcff

Более подробнее про декорирование имен (Name mangling) можно посмотреть тут [3].

Теперь мы можем рассмотреть реализацию семантического анализа для иерархии типов:

Hidden text
// Хранит множество всех уникальных декорированных строк
StringSet< > TypeAST::TypesTable;

void TypeAST::calcMangle() {
  // Ничего не делаем, если декорированное имя для типа уже задано
  if (!MangleName.empty()) {
    return;
  }

  // Для большинства типов 128 байт будет достаточно
  llvm::SmallString< 128 > s;
  llvm::raw_svector_ostream output(s);

  // Пишем декорированное имя в буфер и добавляем в множество имен, 
  // т. к. StringRef требует выделенный блок памяти со строкой
  toMangleBuffer(output);
  TypesTable.insert(output.str());

  // Устанавливаем внутреннее состояние MangleName на основе записи 
  // созданной в множестве имен
  StringSet< >::iterator pos = TypesTable.find(s);
  assert(pos != TypesTable.end());
  MangleName = pos->getKeyData();
}

bool TypeAST::equal(TypeAST* otherType) {
  // Два типа эквивалентны, если их "MangleName" совпадают, для 
  // сложных типов это может быть гораздо быстрее, чем сравнивать
  // их структуру
  assert(!MangleName.empty() && !otherType->MangleName.empty());
  return MangleName == otherType->MangleName;
}

TypeAST* BuiltinTypeAST::semantic(Scope* scope) {
  // Для базовых типов для проверки семантики достаточно произвести
  // декорирование имени типа
  calcMangle();
  return this;
}

bool BuiltinTypeAST::implicitConvertTo(TypeAST* newType) {
  // Список разрешенных преобразований
  static bool convertResults[TI_Float + 1][TI_Float + 1] = {
    // void  bool   int    float
    { false, false, false, false }, // void
    { false, true,  true,  true  }, // bool
    { false, true,  true,  true  }, // int
    { false, true,  true,  true  }, // float
  };

  // Только базовые типы могут быть преобразованы друг в друга
  if (newType->TypeKind > TI_Float) {
    return false;
  }

  return convertResults[TypeKind][newType->TypeKind];
}

void BuiltinTypeAST::toMangleBuffer(llvm::raw_ostream& output) {
  switch (TypeKind) {
    case TI_Void : output << "v"; break;
    case TI_Bool : output << "b"; break;
    case TI_Int : output << "i"; break;
    case TI_Float : output << "f"; break;
    default: assert(0 && "Should never happen"); break;
  }
}

TypeAST* FuncTypeAST::semantic(Scope* scope) {
  if (!ReturnType) {
    // Если у функции не был задан тип возвращаемого значения, то 
    // установить как "void"
    ReturnType = BuiltinTypeAST::get(TypeAST::TI_Void);
  }
  
  // Произвести семантический анализ для типа возвращаемого 
  // значения
  ReturnType = ReturnType->semantic(scope);
  
  // Произвести семантический анализ для всех параметров
  for (ParameterList::iterator it = Params.begin(), 
       end = Params.end(); it != end; ++it) {
    (*it)->Param = (*it)->Param->semantic(scope);
  }

  calcMangle();
  return this;
}

bool FuncTypeAST::implicitConvertTo(TypeAST* newType) {
  return false;
}

void FuncTypeAST::toMangleBuffer(llvm::raw_ostream& output) {
  // Добавляем "v", если функция возвращает "void"
  if (Params.empty()) {
    output << "v";
    return;
  }
  
  ParameterList::iterator it = Params.begin(), end = Params.end();

  // Произвести декорацию имен для всех параметров
  for ( ; it != end; ++it) {
    (*it)->Param->toMangleBuffer(output);
  }
}

На данной стадии у нас есть только несколько базовых типов и семантический анализ для иерархии типов достаточно прост, мы просто генерируем декорированные имена для каждого типа. Но в последующих статьях с добавлением более сложных типов семантический анализ усложнится и список преобразований между типами будет так же расширен.

Семантический анализ выражений

Для начала рассмотрим какие именно функции члены иерархии выражений будут отвечать за семантический анализ этих ветвей AST:

struct ExprAST { 
  ...
  /// Проверка, что это целочисленная константа
  bool isIntConst() { return ExprKind == EI_Int; } 
  /// Проверка, что это константное выражение
  bool isConst() { 
    return ExprKind == EI_Int || ExprKind == EI_Float; 
  }
  /// Проверяем, что константное выражение имеет истинное значение 
  virtual bool isTrue(); 
  /// Проверка на то, что данное выражение может быть использовано
  /// в качестве значения с левой стороны от "="
  virtual bool isLValue(); 
  /// Произвести семантический анализ выражения
  virtual ExprAST *semantic(Scope *scope) = 0; 
  /// Сделать копию ветви дерева
  virtual ExprAST *clone() = 0; 

  TypeAST *ExprType;  ///< тип выражения
};

Так же, как и у типов, ExprAST после семантического анализа может быть изменено (например можно сделать оптимизацию и произвести вычисление константных выражений, но в нашем случае мы будем использовать это для замены некоторых выражений их аналогами, что бы упростить генерацию кода).

Рассмотрим более подробно сам семантический анализ для выражений:

Hidden text
bool ExprAST::isTrue() { 
  return false; 
}

bool ExprAST::isLValue() {
  return false;
}

bool IntExprAST::isTrue() {
  return Val != 0;
}

ExprAST* IntExprAST::semantic(Scope* ) {
  return this;
}

ExprAST* IntExprAST::clone() {
  return new IntExprAST(Loc, Val);
}

bool FloatExprAST::isTrue() {
  return Val != 0.0;
}

ExprAST* FloatExprAST::semantic(Scope* ) {
  return this;
}

ExprAST* FloatExprAST::clone() {
  return new FloatExprAST(Loc, Val);
}

bool IdExprAST::isLValue() {
  return true;
}

ExprAST* IdExprAST::semantic(Scope* scope) {
  // Если "ThisSym" задан, то семантический анализ над данным
  // выражением уже был ранее завершен
  if (!ThisSym) {
    // Ищем объявление в текущей области видимости
    ThisSym = scope->find(Val);

    if (!Val) {
      return this;
    }

    if (!ThisSym) {
      // Объявление не найдено, возвращаем ошибку
      scope->report(Loc, diag::ERR_SemaUndefinedIdentifier,
                    Val->Id);
      return nullptr;
    }
    // Устанавливаем тип данного выражения в соответствии с типом 
    // объявленной переменной
    ExprType = ThisSym->getType();
  }

  return this;
}

ExprAST* IdExprAST::clone() {
  return new IdExprAST(Loc, Val);
}

ExprAST* CastExprAST::semantic(Scope* scope) {
  if (SemaDone) {
    return this;
  }

  // Проверяем, что тип был корректно задан и что это не "void"
  assert(ExprType != nullptr && "Type for cast not set"); 
  if (ExprType->isVoid()) {
    scope->report(Loc, diag::ERR_SemaCastToVoid);
    return nullptr;
  }

  // Производим семантический анализ выражения для преобразования
  Val = Val->semantic(scope);

  // Проверяем, что тип исходного выражения корректный
  if (!Val->ExprType || Val->ExprType->isVoid()) {
    scope->report(Loc, diag::ERR_SemaCastToVoid);
    return nullptr;
  }

  // Запрещаем преобразования функций
  if (isa<FuncTypeAST>(ExprType)
      || isa<FuncTypeAST>(Val->ExprType)) {
    scope->report(Loc, diag::ERR_SemaFunctionInCast);
    return nullptr;
  }

  // Проверяем, что типы совместимы
  if (!Val->ExprType->implicitConvertTo(ExprType)) {
    scope->report(Loc, diag::ERR_SemaInvalidCast);
    return nullptr;
  }

  SemaDone = true;
  return this;
}

ExprAST* CastExprAST::clone() {
  return new CastExprAST(Loc, Val->clone(), ExprType);
}

Т.к. все выражения с одним операндом могут быть переписаны в виде эквивалентного выражения с двумя операндами, то для упрощения кодогенерации при семантическом анализе мы проверяем корректность операнда и возвращаем эквивалентный аналог:

Hidden text
ExprAST* UnaryExprAST::semantic(Scope* scope) {
  // Проверяем, что операнд был корректно задан
  if (!Val) {
    assert(0 && "Invalid expression value");
    return nullptr;
  }

  // Производим семантический анализ для операнда
  Val = Val->semantic(scope);

  // Проверяем корректность типа операнда, т. к. он может быть 
  // "void"
  if (!Val->ExprType || Val->ExprType->isVoid()) {
    scope->report(Loc, diag::ERR_SemaOperandIsVoid);
    return nullptr;
  }

  // Исключаем операции над булевыми значениями
  if (Val->ExprType->isBool() && (Op == tok::Plus 
                                  || Op == tok::Minus)) {
    scope->report(Loc, diag::ERR_SemaInvalidBoolForUnaryOperands);
    return nullptr;
  }

  // Список замен:
  //  +Val to Val
  //  -Val to 0 - Val
  //  ~intVal to intVal ^ -1
  //  ++id to id = id + 1 
  //  --id to id = id - 1
  //  !Val to Val == 0

  ExprAST* result = this;

  // Проверяем тип оператора, для необходимой замены
  switch (Op) {
    // "+" - noop
    case tok::Plus: result = Val; break;

    case tok::Minus: 
      // Преобразовываем в 0 - Val с учетом типа Val
      if (Val->ExprType->isFloat()) {
        result = new BinaryExprAST(Val->Loc, tok::Minus,
                                   new FloatExprAST(Val->Loc, 0),
                                   Val);
      } else {
        result = new BinaryExprAST(Val->Loc, tok::Minus,
                                   new IntExprAST(Val->Loc, 0),
                                   Val);
      }
      break;

    case tok::Tilda:
      // ~ можно применять только к целочисленным выражениям
      if (!Val->ExprType->isInt()) {
        scope->report(Loc,
                      diag::ERR_SemaInvalidOperandForComplemet);
        return nullptr;
      } else {
        // Преобразуем в Val ^ -1
        result = new BinaryExprAST(Val->Loc, tok::BitXor, Val,
                                   new IntExprAST(Val->Loc, -1));
        break;
      }

    case tok::PlusPlus:
    case tok::MinusMinus: {
        // "++" и "--" можно вызывать только для IdExprAST в 
        // качестве операнда и только для целочисленного типа
        if (!Val->ExprType->isInt() || !Val->isLValue()) {
          scope->report(Loc,
                        diag::ERR_SemaInvalidPostfixPrefixOperand);
          return nullptr;
        }
        
        // Необходимо заменить "++" id или "--" id на id = id + 1 
        // или id = id + -1
        ExprAST* val = Val;
        ExprAST* valCopy = Val->clone();
        result = new BinaryExprAST(Val->Loc, tok::Assign, 
          val,
          new BinaryExprAST(Val->Loc, tok::Plus,
            valCopy, 
            new IntExprAST(Val->Loc, 
                           (Op == tok::PlusPlus) ? 1 : -1)));
      }
      break;

    case tok::Not:
      // Заменяем на Val == 0
      result = new BinaryExprAST(Val->Loc, tok::Equal, Val,
                                 new IntExprAST(Val->Loc, 
        0));
      break;

    default:
      // Никогда не должно произойти
      assert(0 && "Invalid unary expression");
      return nullptr;
  }

  if (result != this) {
    // Т.к. старое выражение было заменено, очищаем память и 
    // производим семантический анализ нового выражения
    Val = nullptr;
    delete this;
    return result->semantic(scope);
  }

  return result;
}

ExprAST* UnaryExprAST::clone() {
  return new UnaryExprAST(Loc, Op, Val->clone());
}

Рассмотрим семантический анализ выражений с двумя операндами:

Hidden text
ExprAST* BinaryExprAST::semantic(Scope* scope) {
  // Семантический анализ уже был произведен ранее
  if (ExprType) {
    return this;
  }

  // Производим семантический анализ операнда с левой стороны
  LeftExpr = LeftExpr->semantic(scope);

  // Проверяем на "++" или "--", т. к. эти операции имеют только 
  // один операнд
  if (Op == tok::PlusPlus || Op == tok::MinusMinus) {
    // "++" и "--" можно вызывать только для IdExprAST в качестве 
    // операнда и только для целочисленного типа
    if (!LeftExpr->isLValue() || !LeftExpr->ExprType->isInt()) {
      scope->report(Loc,
                    diag::ERR_SemaInvalidPostfixPrefixOperand);
      return nullptr;
    }

    // Устанавливаем результирующий тип выражения
    ExprType = LeftExpr->ExprType;
    return this;
  }

  // Производим семантический анализ операнда с правой стороны
  RightExpr = RightExpr->semantic(scope);

  // Проверяем, что оба операнда имеют корректные типы
  if (!LeftExpr->ExprType || !RightExpr->ExprType) {
    scope->report(Loc, 
                  diag::ERR_SemaUntypedBinaryExpressionOperands);
    return nullptr;
  }

  // "," имеет специальную обработку и тип выражения совпадает с 
  // типом операнда с правой стороны
  if (Op == tok::Comma) {
    ExprType = RightExpr->ExprType;
    return this;
  }

  // Исключаем операции, если хотя бы один операнд имеет тип "void"
  if (LeftExpr->ExprType->isVoid()
      || RightExpr->ExprType->isVoid()) {
    scope->report(Loc, diag::ERR_SemaOperandIsVoid);
    return nullptr;
  }

  // Проверка на операторы сравнения, т. к. их результат всегда 
  // будет иметь тип "bool"
  switch (Op) {
    case tok::Less:
    case tok::Greater:
    case tok::LessEqual:
    case tok::GreaterEqual:
    case tok::Equal:
    case tok::NotEqual:
      // Если левый операнд "bool", то сначала конвертируем его в 
      // "int"
      if (LeftExpr->ExprType->isBool()) {
        LeftExpr = new CastExprAST(LeftExpr->Loc, LeftExpr, 
          BuiltinTypeAST::get(TypeAST::TI_Int));
        LeftExpr = LeftExpr->semantic(scope);
      }

      // Операнды для "<", "<=", ">", ">=", "==" и "!=" всегда 
      // должны иметь одинаковые типы, если они отличаются, то 
      // нужно сделать преобразование
      if (!LeftExpr->ExprType->equal(RightExpr->ExprType)) {
        RightExpr = new CastExprAST(RightExpr->Loc, RightExpr,
                                    LeftExpr->ExprType);
        RightExpr = RightExpr->semantic(scope);
      }
      
      // Результирующий тип выражения — "bool"
      ExprType = BuiltinTypeAST::get(TypeAST::TI_Bool);
      return this;

    case tok::LogOr:
    case tok::LogAnd:
      // Для логических операций оба операнда должны 
      // конвертироваться в "bool"
      if (!LeftExpr->ExprType->implicitConvertTo(
          BuiltinTypeAST::get(TypeAST::TI_Bool))
        || !RightExpr->ExprType->implicitConvertTo(
          BuiltinTypeAST::get(TypeAST::TI_Bool))) {
        scope->report(Loc, diag::ERR_SemaCantConvertToBoolean);
        return nullptr;
      }

      // Результирующий тип выражения — "bool"
      ExprType = BuiltinTypeAST::get(TypeAST::TI_Bool);
      return this;

    default:
      // Остальные варианты обрабатываем ниже
      break;
  }

  // Если левый операнд "bool", то сначала конвертируем его в "int"
  if (LeftExpr->ExprType == BuiltinTypeAST::get(TypeAST::TI_Bool)) {
    LeftExpr = new CastExprAST(LeftExpr->Loc, LeftExpr, 
      BuiltinTypeAST::get(TypeAST::TI_Int));
    LeftExpr = LeftExpr->semantic(scope);
  }

  // Результирующий тип выражения будет совпадать с типом левого 
  // операнда
  ExprType = LeftExpr->ExprType;

  // Для "=" тоже есть специальная обработка
  if (Op == tok::Assign) {
    // Если типы левого и правого операнда отличаются, то нужно 
    // сделать преобразование
    if (!LeftExpr->ExprType->equal(RightExpr->ExprType)) {
      RightExpr = new CastExprAST(RightExpr->Loc, RightExpr,
                                  LeftExpr->ExprType);
      RightExpr = RightExpr->semantic(scope);
    }

    // Проверяем, что операнд с левой стороны является адресом
    if (!LeftExpr->isLValue()) {
      scope->report(Loc, diag::ERR_SemaMissingLValueInAssignment);
      return nullptr;
    }

    // Выражение корректно, завершаем анализ
    return this;
  }

  // Если операнды имеют различные типы, то нужно произвести 
  // преобразования
  if (!LeftExpr->ExprType->equal(RightExpr->ExprType)) {
    // Если операнд с правой стороны имеет тип "float", то 
    // результат операции тоже будет "float"
    if (RightExpr->ExprType->isFloat()) {
      ExprType = RightExpr->ExprType;
      LeftExpr = new CastExprAST(LeftExpr->Loc, LeftExpr,
                                 RightExpr->ExprType);
      LeftExpr = LeftExpr->semantic(scope);
    } else {
      // Преобразуем операнд с правой стороны к типу операнда с 
      // левой стороны
      RightExpr = new CastExprAST(RightExpr->Loc, RightExpr,
                                  LeftExpr->ExprType);
      RightExpr = RightExpr->semantic(scope);
    }
  }

  // "int" и "float" имеют отличный набор допустимых бинарных 
  // операций
  if (ExprType == BuiltinTypeAST::get(TypeAST::TI_Int)) {
    // Проверяем допустимые операции над "int"
    switch (Op) {
      case tok::Plus:
      case tok::Minus:
      case tok::Mul:
      case tok::Div:
      case tok::Mod:
      case tok::BitOr:
      case tok::BitAnd:
      case tok::BitXor:
      case tok::LShift:
      case tok::RShift:
        return this;

      default:
        // Никогда не должны сюда попасть, если только нет ошибки 
        // в парсере
        assert(0 && "Invalid integral binary operator"); 
        return nullptr;
    }
  } else {
    // Проверяем допустимые операции над "float"
    switch (Op) {
      case tok::Plus: 
      case tok::Minus: 
      case tok::Mul: 
      case tok::Div: 
      case tok::Mod: 
      case tok::Less: 
        return this;

      default:
        // Сообщаем об ошибке, т. к. данная операция не допустима
        scope->report(Loc,
          diag::ERR_SemaInvalidBinaryExpressionForFloatingPoint);
        return nullptr;
    }
  }
}

ExprAST* BinaryExprAST::clone() {
  return new BinaryExprAST(Loc, Op, LeftExpr->clone(), 
    RightExpr ? RightExpr->clone() : nullptr);
}

Семантический анализ тернарного оператор:

Hidden text
bool CondExprAST::isLValue() {
  return IfExpr->isLValue() && ElseExpr->isLValue();
}

ExprAST* CondExprAST::semantic(Scope* scope) {
  if (SemaDone) {
    return this;
  }

  // Производим семантический анализ условия и всех операндов
  Cond = Cond->semantic(scope);
  IfExpr = IfExpr->semantic(scope);
  ElseExpr = ElseExpr->semantic(scope);

  // Проверяем, что условие не является "void"
  if (Cond->ExprType == nullptr || Cond->ExprType->isVoid()) {
    scope->report(Loc, diag::ERR_SemaConditionIsVoid);
    return nullptr;
  }

  // Проверяем, что условие может быть преобразовано в "bool"
  if (!Cond->ExprType->implicitConvertTo(
    BuiltinTypeAST::get(TypeAST::TI_Bool))) {
    scope->report(Loc, diag::ERR_SemaCantConvertToBoolean);
    return nullptr;
  }

  // Результирующий тип совпадает с тем, что задан в ветке с 
  // выражением, если условие истинно
  ExprType = IfExpr->ExprType;

  // Если обе части имеют одинаковые типы, то больше семантический
  // анализ завершен
  if (IfExpr->ExprType->equal(ElseExpr->ExprType))
    SemaDone = true;
    return this;
  }

  // Исключаем вариант, когда один из операндов имеет тип "void", 
  // но разрешаем если оба операнда имеют тип "void"
  if (!IfExpr->ExprType || !ElseExpr->ExprType ||
    IfExpr->ExprType->isVoid() || ElseExpr->ExprType->isVoid()) {
    scope->report(Loc, diag::ERR_SemaOperandIsVoid);
    return nullptr;
  }

  // Приводим типы к единому
  ElseExpr = new CastExprAST(ElseExpr->Loc, ElseExpr, ExprType);
  ElseExpr = ElseExpr->semantic(scope);
  SemaDone = true;

  return this;
}

ExprAST* CondExprAST::clone() {
  return new CondExprAST(Loc, Cond->clone(), IfExpr->clone(),
                         ElseExpr->clone());
}

Семантический анализ вызова функции:

Hidden text
static SymbolAST* resolveFunctionCall(Scope *scope, SymbolAST* func,
                                      CallExprAST* args) {
  // Проверяем, что это функция
  if (isa<FuncDeclAST>(func)) {
    FuncDeclAST* fnc = static_cast< FuncDeclAST* >(func);
    FuncTypeAST* type = static_cast< FuncTypeAST* >(fnc->ThisType);

    // Количество аргументов должно совпадать с количеством 
    // параметров у функции
    if (args->Args.size() != type->Params.size()) {
      scope->report(args->Loc, 
                    diag::ERR_SemaInvalidNumberOfArgumentsInCall);
      return nullptr;
    }

    ExprList::iterator arg = args->Args.begin();
    ParameterList::iterator it = type->Params.begin();

    // Проверяем все аргументы
    for (ParameterList::iterator end = type->Params.end();
         it != end; ++it, ++arg) {
      // Проверяем, что аргумент может быть использован для вызова
      // и диагностируем об ошибке, если нет
      if (!(*arg)->ExprType->implicitConvertTo((*it)->Param)) {
        scope->report(args->Loc,
                      diag::ERR_SemaInvalidTypesOfArgumentsInCall);
        return nullptr;
      }
    }

    // Вызов функции может быть произведен с данными аргументами
    return func;
  }

  return nullptr;
}

ExprAST* CallExprAST::semantic(Scope* scope) {
  if (ExprType) {
    return this;
  }

  // Производим семантический анализ выражения до "("
  Callee = Callee->semantic(scope);

  // Мы можем использовать только IdExprAST в качестве выражения 
  // для вызова
  if (isa<IdExprAST>(Callee)) {
    SymbolAST* sym = ((IdExprAST*)Callee)->ThisSym;

    // Идентификатор может ссылаться только на функцию
    if (isa<FuncDeclAST>(sym)) {
      TypeAST* returnType = nullptr;

      // Производим семантический анализ для всех аргументов 
      // функции
      for (ExprList::iterator arg = Args.begin(), end = Args.end();
           arg != end; ++arg) {
        *arg = (*arg)->semantic(scope);
      }
      
      // Ищем функцию для вызова
      if (SymbolAST* newSym = resolveFunctionCall(scope, sym, 
                                                  this)) {
        FuncDeclAST* fnc = static_cast< FuncDeclAST* >(newSym);
        FuncTypeAST* type = static_cast< FuncTypeAST* >(
          fnc->ThisType);
        ExprList::iterator arg = Args.begin();
        ParameterList::iterator it = type->Params.begin();

        // Производим сопоставление аргументов и параметров
        for (ParameterList::iterator end = type->Params.end();
             it != end; ++it, ++arg) {
          // Если тип аргумента отличается от типа параметра,
          // производим преобразование типа
          if (!(*arg)->ExprType->equal((*it)->Param)) {
            ExprAST* oldArg = (*arg);
            *arg = new CastExprAST(oldArg->Loc, oldArg->clone(), 
                                   (*it)->Param);
            *arg = (*arg)->semantic(scope);
            delete oldArg;
          }
        }

        // Определяем тип возвращаемого значения и устанавливаем 
        // тип для результата вызова функции
        if (!returnType) {
          ExprType = ((FuncDeclAST*)newSym)->ReturnType;
        } else {
          ExprType = returnType;
        }

        // Устанавливаем объявление функции для вызова для 
        // дальнейшей работы
        CallFunc = newSym;
        return this;
      }
    }
  }
  // Диагностируем ошибку
  scope->report(Loc, diag::ERR_SemaInvalidArgumentsForCall);
  return nullptr;
}

ExprAST* CallExprAST::clone() {
  ExprList exprs;
  ExprList::iterator it = Args.begin();
  ExprList::iterator end = Args.end();

  for ( ; it != end; ++it) {
    exprs.push_back((*it)->clone());
  }

  return new CallExprAST(Loc, Callee->clone(), exprs);
}

Семантический анализ инструкций

Для начала рассмотрим какие именно функции члены иерархии инструкций будут отвечать за семантический анализ этих ветвей AST:

struct StmtAST { 
  ...
  /// Проверяет есть ли выход из функции в данной ветви дерева
  virtual bool hasReturn(); 
  /// Проверяет есть ли инструкция выхода из цикла или возврат из функции в 
  /// данной ветви дерева
  virtual bool hasJump(); 
  /// Произвести семантический анализ для инструкции
  StmtAST* semantic(Scope* scope); 
  /// Произвести семантический анализ для инструкции (должна быть реализована 
  // в потомках,  вызывается только через "semantic"
  virtual StmtAST* doSemantic(Scope* scope); 
   
  int SemaState; ///< стадия семантического анализа для текущей инструкции
}; 

Так же, как и у типов и выражений, StmtAST после семантического анализа может быть изменено (например одна инструкция может быть реализована через другую).

Один из важных классов для работы семантического анализа (и генерации кода) является LandingPadAST (более подробное описание зачем он нужен рассмотрим в следующей части серии, на данный момент приведу только те члены класса, которые нужны для семантического анализа):

struct LandingPadAST { 
  LandingPadAST* Prev; ///< родительский LandingPadAST
  StmtAST* OwnerBlock; ///< блок к которому относится данный LandingPadAST
  int Breaks; ///< количество инструкций выхода из цикла в ветви дерева
  /// количество инструкций возврата из функции в ветви дерева
  int Returns;
  /// количество инструкций перехода к следующей итерации цикла в ветви дерева
  int Continues; 
  bool IsLoop; ///< LandingPadAST находится частью цикла
};

Рассмотрим более подробно сам семантический анализ для инструкций:

Hidden text
bool StmtAST::hasReturn() {
  return false;
}

bool StmtAST::hasJump() {
  return false;
}

StmtAST* StmtAST::semantic(Scope* scope) {
  // Защита от повторного вызова
  if (SemaState > 0) {
    return this;
  }

  ++SemaState;
  return doSemantic(scope);
}

StmtAST* StmtAST::doSemantic(Scope* ) {
  assert(0 && "StmtAST::semantic should never be reached");
  return this;
}

StmtAST* ExprStmtAST::doSemantic(Scope* scope) {
  if (Expr) {
    // Проверка семантики выражения
    Expr = Expr->semantic(scope);

    // После окончания семантического анализа хранимого выражения
    // у него должен быть задан тип
    if (!Expr->ExprType) {
      scope->report(Loc, diag::ERR_SemaNoTypeForExpression);
      return nullptr;
    }
  }

  return this;
}

bool BlockStmtAST::hasReturn() {
  return HasReturn;
}

bool BlockStmtAST::hasJump() {
  return HasJump;
}

StmtAST* BlockStmtAST::doSemantic(Scope* scope) {
  // Для блока мы должны создать новую область видимости
  ThisBlock = new ScopeSymbol(Loc, SymbolAST::SI_Block, nullptr);
  Scope* s = scope->push((ScopeSymbol*)ThisBlock);
  
  // Создать LandingPadAST для данного блока
  LandingPad = new LandingPadAST(s->LandingPad);
  LandingPad->OwnerBlock = this;
  s->LandingPad = LandingPad;

  ExprList args;

  // Проверяем все ветви дерева принадлежащие данному блоку
  for (StmtList::iterator it = Body.begin(), end = Body.end();
       it != end; ++it) {
    // Если в предыдущей инструкции был "break", "continue" или 
    // "return", то диагностируем об ошибке (предотвращаем 
    // появление кода, который не может быть достижимым
    if (HasJump) {
      scope->report(Loc, diag::ERR_SemaDeadCode);
      return nullptr;
    }

    // Проверяем семантику вложенной инструкции
    *it = (*it)->semantic(s);

    // Проверяем, что это "break", "continue" или "return" 
    if ((*it)->isJump()) {
      HasJump = true;

      // Проверяем, что это "return"
      if ((*it)->hasReturn()) {
        HasReturn = true;
      }
    } else {
      // Это обычная инструкция, но все равно проверяем, наличие 
      // "break", "continue" или "return" в дочерних ветках
      // инструкции
      HasJump = (*it)->hasJump();
      HasReturn = (*it)->hasReturn();
    }
  }

  // Удаляем область видимости
  s->pop();

  return this;
}

StmtAST* DeclStmtAST::doSemantic(Scope* scope) {
  // Производим семантический анализ для всех объявлений
  for (SymbolList::iterator it = Decls.begin(), end = Decls.end();
       it != end; ++it) {
    (*it)->semantic(scope);
    (*it)->semantic2(scope);
    (*it)->semantic3(scope);
    (*it)->semantic4(scope);
    (*it)->semantic5(scope);
  }

  return this;
}

bool BreakStmtAST::hasJump() {
  return true;
}

StmtAST* BreakStmtAST::doSemantic(Scope* scope) {
  // Проверяем, что мы находимся в цикле, т. к. "break" может быть
  // только в цикле
  if (!scope->BreakLoc) {
    scope->report(Loc, diag::ERR_SemaInvalidJumpStatement);
    return nullptr;
  }

  // Запоминаем местоположение точки, куда нужно перевести 
  // управление для выхода из цикла
  BreakLoc = scope->LandingPad;
  ++BreakLoc->Breaks;
  return this;
}

// ContinueStmtAST implementation
bool ContinueStmtAST::hasJump() {
  return true;
}

StmtAST* ContinueStmtAST::doSemantic(Scope* scope) {
  // Проверяем, что мы находимся в цикле, т. к. "continue" может 
  // быть только в цикле
  if (!scope->ContinueLoc) {
    scope->report(Loc, diag::ERR_SemaInvalidJumpStatement);
    return nullptr;
  }

  // Запоминаем местоположение точки, куда нужно перевести
  // управление для перехода к следующей итерации цикла
  ContinueLoc = scope->LandingPad;
  ++ContinueLoc->Continues;
  return this;
}

bool ReturnStmtAST::hasReturn() {
  return true;
}

bool ReturnStmtAST::hasJump() {
  return true;
}

StmtAST* ReturnStmtAST::doSemantic(Scope* scope) {
  assert(scope→LandingPad);
  // Сохраняем местоположения точки, куда нужно перевести 
  // управление для выхода из функции
  ReturnLoc = scope->LandingPad;
  ++ReturnLoc->Returns;
  // Проверка наличия возвращаемого значения
  if (Expr) {
    // Если тип возвращаемого значения функции "void", то 
    // сигнализируем об ошибке
    if (!scope->EnclosedFunc->ReturnType
        || scope->EnclosedFunc->ReturnType->isVoid()) {
      scope->report(Loc, diag::ERR_SemaReturnValueInVoidFunction);
      return nullptr;
    }

    // Производим семантический анализ возвращаемого значения
    Expr = Expr->semantic(scope);

    // Если тип возвращаемого значения не совпадает с типом
    // возвращаемого значения самой функции, то мы должны 
    // произвести преобразование типов
    if (!scope->EnclosedFunc->ReturnType->equal(Expr->ExprType)) {
      Expr = new CastExprAST(Loc, Expr,
                             scope->EnclosedFunc->ReturnType);
      Expr = Expr->semantic(scope);
    }

    return this;
  }

  // У нас нет выражения для возврата. Проверяем, что тип 
  // возвращаемого значения самой функции "void" и сигнализируем
  // об ошибке, если он отличен от "void"
  if (scope->EnclosedFunc->ReturnType
      && !scope->EnclosedFunc->ReturnType->isVoid()) {
    scope->report(Loc, diag::ERR_SemaReturnVoidFromFunction);
    return nullptr;
  }

  return this;
}

bool WhileStmtAST::hasReturn() {
  // Всегда возвращаем "false", т. к. может быть 0 итераций
  return false;
}

StmtAST* WhileStmtAST::doSemantic(Scope* scope) {
  // Производим семантический анализ условия цикла
  Cond = Cond->semantic(scope);

  // Условие цикла должно иметь не "void" тип
  if (!Cond->ExprType || Cond->ExprType->isVoid()) {
    scope->report(Loc, diag::ERR_SemaConditionIsVoid);
    return nullptr;
  }

  // Проверяем, что условие цикла может быть преобразовано в "bool"
  if (!Cond->ExprType->implicitConvertTo(
    BuiltinTypeAST::get(TypeAST::TI_Bool))) {
    scope->report(Loc, diag::ERR_SemaCantConvertToBoolean);
    return nullptr;
  }

  // Делаем копии для точек возврата из цикла и перехода к 
  // следующей итерации
  StmtAST* oldBreak = scope->BreakLoc;
  StmtAST* oldContinue = scope->ContinueLoc;

  // Устанавливаем данный цикл в качестве точек возврата из цикла
  // и перехода к следующей итерации
  scope->BreakLoc = this;
  scope->ContinueLoc = this;
  // Создаем новую LandingPadAST для всех вложенных инструкций
  LandingPad = new LandingPadAST(scope->LandingPad);
  LandingPad->IsLoop = true;
  scope->LandingPad = LandingPad;
  
  // Производим семантический анализ тела цикла
  Body = Body->semantic(scope);

  // Восстанавливаем предыдущие значения для точек возврата
  scope->BreakLoc = oldBreak;
  scope->ContinueLoc = oldContinue;
  scope->LandingPad = LandingPad->Prev;

  if (PostExpr) {
    // Производим семантический анализ для "PostExpr", который был
    // создан в процессе конвертации цикла "for" в цикл "while" 
    PostExpr = PostExpr->semantic(scope);
  }

  return this;
}

StmtAST* ForStmtAST::doSemantic(Scope* scope) {
  // Заменяем цикл "for" эквивалентным аналогом цикла "while", что 
  // бы упростить генерацию кода, но предварительно проверив
  // семантику
  // {
  //   init
  //   while (cond) {
  //     body
  //     continueZone: post
  //   }
  // }

  StmtAST* init = nullptr;

  // Если в цикле задано выражение для инициализации или объявлены
  // переменные цикла, то нужно создать на их основе 
  // соответствующие инструкции
  if (InitExpr) {
    init = new ExprStmtAST(Loc, InitExpr);
  } else if (!InitDecls.empty()) {
    init = new DeclStmtAST(Loc, InitDecls);
  }
 
  if (Cond) {
    // У нас есть условие выхода из цикла
    StmtList stmts;

    // Если у нас есть блок инициализации цикла, то добавляем его 
    // к телу новой конструкции
    if (init) {
      stmts.push_back(init);
    }

    // Создаем новый цикл "while" на основе данного цикла "for" и 
    // добавляем его к списку инструкций в блоке
    WhileStmtAST* newLoop = new WhileStmtAST(Loc, Cond, Body);
    newLoop->PostExpr = Post;
    stmts.push_back(newLoop);

    // Очищаем и удаляем данную ветку
    InitExpr = nullptr;
    InitDecls.clear();
    Cond = nullptr;
    Post = nullptr;
    Body = nullptr;
    delete this;

    // Создаем новы блочный элемент для нового цикла и производим
    // его семантический анализ
    StmtAST* res = new BlockStmtAST(Loc, stmts);
    return res->semantic(scope);
  } else {
    // У нас нет условия выхода из цикла
    StmtList stmts;

    // Если у нас есть блок инициализации цикла, то добавляем его 
    // к телу новой конструкции
    if (init) {
      stmts.push_back(init);
    }

    // Создаем новый цикл "while" на основе данного цикла "for" и 
    // добавляем его к списку инструкций в блоке
    WhileStmtAST* newLoop = new WhileStmtAST(Loc,
                                             new IntExprAST(Loc, 1),
                                             Body);
    newLoop->PostExpr = Post;
    stmts.push_back(newLoop);

    // Очищаем и удаляем данную ветку
    InitExpr = nullptr;
    InitDecls.clear();
    Cond = nullptr;
    Post = nullptr;
    Body = nullptr;
    delete this;

    // Создаем новы блочный элемент для нового цикла и производим 
    // его семантический анализ
    StmtAST* res = new BlockStmtAST(Loc, stmts);
    return res->semantic(scope);
  }
}

bool IfStmtAST::hasReturn() {
  if (!ElseBody) {
    return false;
  }

  // Возвращаем "true" только если обе ветки имеют инструкции 
  // возврата из функции
  return ThenBody->hasReturn() && ElseBody->hasReturn();
}

bool IfStmtAST::hasJump() {
  if (!ElseBody) {
    return false;
  }

  // Возвращаем "true" только если обе ветки имеют инструкции 
  // возврата из функции или выхода из цикла
  return ThenBody->hasJump() && ElseBody->hasJump();
}

StmtAST* IfStmtAST::doSemantic(Scope* scope) {
  // Производим семантический анализ условия
  Cond = Cond->semantic(scope);

  // Запрещаем условия с типом "void"
  if (!Cond->ExprType
      || Cond->ExprType == BuiltinTypeAST::get(TypeAST::TI_Void)) {
    scope->report(Loc, diag::ERR_SemaConditionIsVoid);
    return nullptr;
  }

  // Проверяем, что условие может быть преобразовано в "bool"
  if (!Cond->ExprType->implicitConvertTo(
    BuiltinTypeAST::get(TypeAST::TI_Bool))) {
    scope->report(Loc, diag::ERR_SemaCantConvertToBoolean);
    return nullptr;
  }

  // Создаем новый LandingPadAST
  LandingPad = new LandingPadAST(scope->LandingPad);
  scope->LandingPad = LandingPad;

  // Производим семантический анализ ветки, если условие истинно
  ThenBody = ThenBody->semantic(scope);

  // Производим семантический анализ ветки, если условие ложно,
  // если она есть
  if (ElseBody) {
    ElseBody = ElseBody->semantic(scope);
  }

  // Восстанавливаем старый LandingPadAST
  scope->LandingPad = LandingPad->Prev;

  return this;
}

Семантический анализ для объявлений

Для начала рассмотрим какие именно функции члены иерархии объявлений будут отвечать за семантический анализ этих ветвей AST:

struct SymbolAST { 
  /// Получить тип объявления
  virtual TypeAST *getType(); 
  /// Произвести 1 фазу семантического анализа
  void semantic(Scope *scope); 
  /// Произвести 2 фазу семантического анализа
  void semantic2(Scope *scope); 
  /// Произвести 3 фазу семантического анализа
  void semantic3(Scope *scope); 
  /// Произвести 4 фазу семантического анализа
  void semantic4(Scope *scope); 
  /// Произвести 5 фазу семантического анализа
  void semantic5(Scope *scope); 

  /// Произвести 1 фазу семантического анализа (должна быть переопределена в 
  /// потомках, вызывается только через "semantic")
  virtual void doSemantic(Scope *scope); 
  /// Произвести 2 фазу семантического анализа (может быть переопределена в
  /// потомках, вызывается только через "semantic2")
  virtual void doSemantic2(Scope *scope); 
  /// Произвести 3 фазу семантического анализа (может быть переопределена в
  /// потомках, вызывается только через "semantic3")
  virtual void doSemantic3(Scope *scope); 
  /// Произвести 4 фазу семантического анализа (может быть переопределена в 
  /// потомках, вызывается только через "semantic4")
  virtual void doSemantic4(Scope *scope); 
  /// Произвести 5 фазу семантического анализа (может быть переопределена в
  /// потомках, вызывается только через "semantic5")
  virtual void doSemantic5(Scope *scope); 

  /// Поиск дочернего объявления ("flags" — 1 если не нужен поиск в 
  /// родительских классах) 
  virtual SymbolAST *find(Name *id, int flags = 0); 
  /// область видимости, в которой было объявлено данное объявление
  SymbolAST *Parent;
  int SemaState;   ///< текущая стадия семантического анализа
}; 

Из-за правил поиска идентификаторов в simple, весь семантический анализ был разбит на 5 фаз:

  1. Создание списка всех объявлений в области видимости;

  2. Разрешение типов базовых классов;

  3. Разрешение типов для имен переменных и функций членов структур и классов;

  4. Построение таблиц виртуальных функций, конструкторов и деструкторов;

  5. Анализ функций и их тел.

Разбивка семантики на несколько фаз позволяет упростить грамматику языка, т. к. не нужно вводить дополнительные конструкции для объявления и определения (т. е. введение имени в программу (что бы оно могло быть использовано) и описание ее реализации (т. е. для классов это описание всех его функций и переменных членов, а для функций ее тело). Например в C++ можно сперва объявить класс (написать "class A;"), что позволяет использовать имя этого класса в других объявлениях (например в качестве параметра функции), а потом в другой части исходного кода произвести определение — описать все функции и переменные члены класса. При разбивке семантического анализа на части, такие разграничения объявления и определения на разные сущности просто не нужны, т. к. даже циклические зависимости могут быть спокойно разрешены, т. к. в каждой фазе происходят действия, которые позволят продолжить анализ на следующих стадиях.

Ниже рассмотрим сам семантический анализ для объявлений:

Hidden text
TypeAST* SymbolAST::getType() {
  assert(0 && "SymbolAST::getType should never be reached");
  return nullptr;
}

void SymbolAST::semantic(Scope* scope) {
  // Пропускаем семантический анализ для этой фазы, если она уже 
  // была завершена
  if (SemaState > 0) {
    return;
  }

  // Произвести семантический анализ и переход к следующей стадии
  doSemantic(scope);
  ++SemaState;
}

void SymbolAST::semantic2(Scope* scope) {
  // Пропускаем семантический анализ для этой фазы, если она уже 
  // была завершена
  assert(SemaState >= 1);
  if (SemaState > 1) {
    return;
  }

  // Произвести семантический анализ и переход к следующей стадии
  doSemantic2(scope);
  ++SemaState;
}

void SymbolAST::semantic3(Scope* scope) {
  // Пропускаем семантический анализ для этой фазы, если она уже 
  // была завершена
  assert(SemaState >= 2);
  if (SemaState > 2) {
    return;
  }

  // Произвести семантический анализ и переход к следующей стадии
  doSemantic3(scope);
  ++SemaState;
}

void SymbolAST::semantic4(Scope* scope) {
  // Пропускаем семантический анализ для этой фазы, если она уже 
  // была завершена
  assert(SemaState >= 3);
  if (SemaState > 3) {
    return;
  }

  // Произвести семантический анализ и переход к следующей стадии
  doSemantic4(scope);
  ++SemaState;
}

void SymbolAST::semantic5(Scope* scope) {
  // Пропускаем семантический анализ для этой фазы, если она уже 
  // была завершена
  assert(SemaState >= 4);
  if (SemaState > 4) {
    return;
  }

  // Произвести семантический анализ и переход к следующей стадии
  doSemantic5(scope);
  ++SemaState;
}

void SymbolAST::doSemantic(Scope* ) {
  // Данная функция обязательна для реализации в дочерних классах
  assert(0 && "SymbolAST::semantic should never be reached");
}

void SymbolAST::doSemantic2(Scope* ) {
  // По умолчанию игнорируем данную фазу
}

void SymbolAST::doSemantic3(Scope* ) {
  // По умолчанию игнорируем данную фазу
}

void SymbolAST::doSemantic4(Scope* ) {
  // По умолчанию игнорируем данную фазу
}

void SymbolAST::doSemantic5(Scope* ) {
  // По умолчанию игнорируем данную фазу
}

SymbolAST* SymbolAST::find(Name* id, int flags) {
  // В базовой реализации только проверяем имя самой сущности
  if (id == Id) {
    return this;
  }

  return nullptr;
}

TypeAST* VarDeclAST::getType() {
  return ThisType;
}

void VarDeclAST::doSemantic(Scope* scope) {
  // Исключаем повторные объявления переменных
  if (scope->find(Id)) {
    scope->report(Loc, diag::ERR_SemaIdentifierRedefinition);
    return;
  }
  // Добавляем переменную в список объявленных переменных в текущей
  // области видимости
  ((ScopeSymbol*)scope->CurScope)->Decls[Id] = this;
  Parent = scope->CurScope;
  // Так же добавляем переменную к списку объявленных переменных в
  // функции, если она была объявлена в функции (нужны для 
  // генерации кода)
  if (scope->EnclosedFunc) {
    scope->EnclosedFunc->FuncVars.push_back(this);
  }
  // Производим семантический анализ для типа переменной
  ThisType = ThisType->semantic(scope);
}

void VarDeclAST::doSemantic3(Scope* scope) {
  // Проверяем наличие инициализатора
  if (Val) {
    // Производим семантический анализ инициализирующего выражения
    Val = Val->semantic(scope);

    // Запрещаем использования выражений с типов "void" в 
    // инициализации
    if (!Val->ExprType || Val->ExprType->isVoid()) {
      scope->report(Loc, diag::ERR_SemaVoidInitializer);
      return;
    }

    // Если типы не совпадают, то добавляем преобразование
    if (!Val->ExprType->equal(ThisType)) {
      Val = new CastExprAST(Loc, Val, ThisType);
      Val = Val->semantic(scope);
    }
  }
}

ScopeSymbol::~ScopeSymbol() {
}

SymbolAST* ScopeSymbol::find(Name* id, int /*flags*/) {
  // Производим поиск в объявленных в данном блоке объявлениях
  SymbolMap::iterator it = Decls.find(id);

  if (it == Decls.end()) {
    return nullptr;
  }

  return it->second;
}

TypeAST* ParameterSymbolAST::getType() {
  return Param->Param;
}

void ParameterSymbolAST::doSemantic(Scope* ) {
}

SymbolAST* ParameterSymbolAST::find(Name* id, int ) {
  if (id == Param->Id) {
    return this;
  }

  return nullptr;
}

TypeAST* FuncDeclAST::getType() {
  return ThisType;
}

void FuncDeclAST::doSemantic(Scope* scope) {
  // Производим семантический анализ для прототипа функции
  ThisType = ThisType->semantic(scope);
  Parent = scope->CurScope;
  // Настраиваем тип возвращаемого значения
  ReturnType = ((FuncTypeAST*)ThisType)->ReturnType;
  
  // Отдельная проверка для функции "main"
  if (Id->Length == 4 && memcmp(Id->Id, "main", 4) == 0) {
    FuncTypeAST* thisType = (FuncTypeAST*)ThisType;

    // Должна не иметь параметров
    if (thisType->Params.size()) {
      scope->report(Loc, diag::ERR_SemaMainParameters);
      return;
    }
    
    // Должна возвращать "float"
    if (ReturnType != BuiltinTypeAST::get(TypeAST::TI_Float)) {
      scope->report(Loc, diag::ERR_SemaMainReturnType);
      return;
    }
  }

  // Проверяем, что идентификатор еще не был объявлен ранее
  if (SymbolAST* fncOverload = scope->findMember(Id, 1)) {
    scope->report(Loc, diag::ERR_SemaFunctionRedefined, Id->Id);
    return;
  }

  // Добавляем функцию к списку объявлений
  ((ScopeSymbol*)Parent)->Decls[Id] = this;
}

void FuncDeclAST::doSemantic5(Scope* scope) {
  FuncTypeAST* func = (FuncTypeAST*)ThisType;
  // Проверяем наличие тела функции (если его нет, то это прототип)
  if (Body) {
    // Создаем новую область видимости и устанавливаем текущую
    // функции в данной области видимости
    Scope* s = scope->push(this);
    s->EnclosedFunc = this;

    // Производим проверку всех параметров функции
    for (ParameterList::iterator it = func->Params.begin(),
         end = func->Params.end(); it != end; ++it) {
      ParameterAST* p = *it;
      // Особая обработка для именованных параметров
      if (p->Id) {
        // Запрещаем переопределение
        if (find(p->Id)) {
          scope->report(Loc, diag::ERR_SemaIdentifierRedefinition);
          return;
        }

        // Для каждого параметра в прототипе, создаем отдельную 
        // переменную в самой функции
        SymbolAST* newSym = new ParameterSymbolAST(p);
        Decls[p->Id] = newSym;
        FuncVars.push_back(newSym);
      }
    }

    // Устанавливаем новый LandingPadAST
    LandingPadAST* oldLandingPad = s->LandingPad;
    LandingPad = new LandingPadAST();
    s->LandingPad = LandingPad;

    // Производим семантический анализ тела функции
    Body = Body->semantic(s);

    // Восстанавливаем старый LandingPadAST
    s->LandingPad = oldLandingPad;

    // Проверяем, что функция с типов возвращаемого значения 
    // отличным от "void" вернуло значение
    if (!ReturnType->isVoid() && !Body->hasReturn()) {
      scope->report(Loc,
                    diag::ERR_SemaMissingReturnValueInFunction);
      return;
    }

    // Удаляем область видимости для данной функции
    s->pop();
  }
}

// Функции, которые будут доступны из simple
extern "C" void lle_X_printDouble(double val) {
  outs() << val;
}

extern "C" void lle_X_printLine() {
  outs() << "\n";
}

/// Добавить прототип функции и связать ее с функцией C++
FuncDeclAST* addDynamicFunc(const char* protoString,
                            const char* newName,
                            ModuleDeclAST* modDecl,
                            void* fncPtr) {
  // Разбор прототипа функции
  FuncDeclAST* func = (FuncDeclAST*)parseFuncProto(protoString);
  // Добавляем функцию и указываем, что ее компиляция уже была 
  // произведена
  func->CodeValue = Function::Create(
    (FunctionType*)func->ThisType->getType(),
    Function::ExternalLinkage,
    Twine(newName),
    getSLContext().TheModule
  );
  func->Compiled = true;
  modDecl->Members.insert(modDecl->Members.begin(), func);

  // Делаем функцию доступной из LLVM
  ExitOnError ExitOnErr;

  ExitOnErr(getJIT().addSymbol(newName, fncPtr));

  return func;
}

void initRuntimeFuncs(ModuleDeclAST* modDecl) {
  addDynamicFunc("fn print(_: float)", "lle_X_printDouble", 
                 modDecl, (void*)lle_X_printDouble);
  addDynamicFunc("fn printLn()", "lle_X_printLine",
                 modDecl, (void*)lle_X_printLine);
}

void ModuleDeclAST::semantic() {
  Scope s(this);
  
  // Инициализация runtime функций
  initRuntimeFuncs(this);

  // Производим семантический анализ всех базовых типов
  for (int i = TypeAST::TI_Void; i <= TypeAST::TI_Float; ++i) {
    BuiltinTypeAST::get(i)->semantic(&s);
  }

  // Производим семантический анализ для всех объявлений 
  // (кроме функций)
  for (SymbolList::iterator it = Members.begin(),
       end = Members.end(); it != end; ++it) {
    if (!isa<FuncDeclAST>(*it))
      (*it)->semantic(&s);
  }

  // Производим семантический анализ всех функций
  for (SymbolList::iterator it = Members.begin(),
       end = Members.end(); it != end; ++it) {
    if (isa<FuncDeclAST>(*it))
      (*it)->semantic(&s);
  }

  // Запускаем 2-ю фазу семантического анализа для всех объявлений
  for (SymbolList::iterator it = Members.begin(),
       end = Members.end(); it != end; ++it) {
    (*it)->semantic2(&s);
  }

  // Запускаем 3-ю фазу семантического анализа для всех объявлений
  for (SymbolList::iterator it = Members.begin(),
       end = Members.end(); it != end; ++it) {
    (*it)->semantic3(&s);
  }

  // Запускаем 4-ю фазу семантического анализа для всех объявлений
  for (SymbolList::iterator it = Members.begin(),
       end = Members.end(); it != end; ++it) {
    (*it)->semantic4(&s);
  }

  // Запускаем 5-ю фазу семантического анализа для всех объявлений
  for (SymbolList::iterator it = Members.begin(),
       end = Members.end(); it != end; ++it) {
    (*it)->semantic5(&s);
  }
}

Заключение

В данной части мы рассмотрели семантический анализ программы на языке simple, произвели проверку ее корректности и подготовили наше промежуточное представление к заключительной части — генерации кода.

В статье мы научились изменять изначальное представление программы его аналогом, что позволит упростить многие части генерации кода (например автоматическое создание преобразований типов, где они были необходимы, позволит не задумываться о преобразовании типов на стадии генерации кода, т. к. этим будет заниматься отдельный класс). Автоматическая генерация и модификация дерева может быть полезна при решении многих задач в проектировании своего языка. В последующих частях мы еще не раз будем использовать данный подход, например для генерации кода для вызова конструкторов и деструкторов для объектов класса, генерации конструкторов по умолчанию, а так же автоматический вызов конструкторов и деструкторов для родительских классов. В более сложных языках программирования данный подход может быть применен для генерации замыканий, итераторов и многих других высокоуровневых конструкциях.

Разбиение семантического анализа на фазы так же позволяет делать язык более выразительным и дает возможность убрать из него многие ограничения, которые при однопроходном варианте приходилось бы решать другими способами.

Полный исходный код доступен на gihub. В следующей статье мы закончим реализацию базовой версии языка simple и добавим генерацию кода для LLVM IR.

Полезные ссылки

  1. https://en.wikipedia.org/wiki/Scope_(computer_science)

  2. https://en.wikipedia.org/wiki/Type_system

  3. https://en.wikipedia.org/wiki/Name_mangling

Комментарии (19)


  1. WASD1
    05.02.2023 05:17

    Здравствуйте спасибо за статью.
    Позвольте пару уточнений:
    1. А что представляет из себя сам язык (или это "general C++ подобное (без мультинаследования надеюсь) в образовательных целях), а то как-то обсуждать технические моменты без а) общей идеи б) технической реализации -- не очень удобно.

    2. Вы в прошлой статье обмолвились, что "компилятор делится на 2 части front-end и back-end". Мне кажется, что сегодня уже лучше говорить как в LLVM: front-end (всё до преобразования в IR), middle-end (аппаратно-независимые оптимизации над IR), back-end (аппаратно-зависимые оптимизации над IR и планирование+кодогенерация).




    1. DCSinpo Автор
      05.02.2023 10:13

      Спасибо.

      1. В первой статье я дал пример программы, которую можно будет написать по завершению всего цикла статей. Но если в общих чертах, то:

      • Язык имеет синтаксис чем-то схожий с Typescript, Rust (т. е. С++ подобный синтаксис, с упрощенным видом определений переменных и функций, для исключения неоднозначностей языка);

      • Есть несколько базовых типов void, int, float, char и string (так же есть bool, но пользователь не может объявлять переменные данного типа). Все переменные должны объявлены с указанием их типа;

      • На основе базовых типов пользователь может создавать массивы, указатели, функции, структуры и классы, функции поддерживают перегрузку по количеству и типу принимаемых параметров;

      • Язык поддерживает одиночное наследование и в языке есть виртуальные функции, в случае необходимости компилятор генерирует конструкторы и деструкторы, а так же их вызовы при создании и уничтожении объектов;

      • Язык поддерживает new, del для работы с памятью;

      • В языке есть if, while, for, break, continue и return;

      • Поддерживает все арифметические операции над типами, включая арифметику над указателями.

      Hidden text
      fn printLn(s1: string, s2: string) {
        print(s1);
        print(s2);
        printLn("");
      }
      
      fn printLn(s: string, i: int) {
        print(s);
        print(i);
        printLn("");
      }
      
      class Shape {
        virt draw() {
        }
      }
      
      class Square extends Shape {
        side: int = 0;
      
        new(s: int) {
          side = s;
        }
      
        impl draw() {
          printLn("Square.draw side: ", side);
        }
      }
      
      class Circle extends Shape {
        radius: int = 0;
      
        new(r: int) {
          radius = r;
        }
      
        impl draw() {
          printLn("Circle.draw radius: ", radius);
        }
      }
      
      fn main(): float {
        for let i: int = 0; i < 10; ++i {
          let p: Shape* = 0;
          
          if i % 2 == 0 {
            p = new Square((i + 1) * 10);
          } else {
            p = new Circle((i + 1) * 5);
          }
      
          p.draw();
          del p;
        }
      
        return 0.0;
      }

      В первых 3-х частях серии рассматривается только подмножество языка в котором есть только базовые типы int, float, void и bool и функции (без их перегрузки), все остальное будет добавлено в последующих статьях.

      1. Тут я полностью согласен с вами.


      1. WASD1
        05.02.2023 15:12

        Спасибо.
        Удачи вам, в целом масштабный обучающий\хобби проект, искренне желаю вам довести его до конца и не пополнять "хабровское кладбище недоделанных прекрасных проектов".

        ПС
        Я правильно понял, что язык в целом учебный и служит целью "чтобы было вокруг чего писать компилятор" и какие-то каверзные вопросы (почему проблему if {} if {} else {} не поправили) вам задавать не стоит?


        1. DCSinpo Автор
          05.02.2023 15:36

          1. Язык изначально задумывался для серии статей, но т. к. в рамках серии статей достаточно сложно рассмотреть что-то по сложности сопоставимое с C++, Rust, Swift и другими языками, то я решил сделать что-то более простое, что можно было написать за 2 недели работы. Но этой базы уже достаточно для того, что бы реализовать что-то более сложное путем добавления нужного функционала. (Финальная версия уже принимает тот код, который я привел в предыдущем комментарии. Пока я решил не публиковать полную версию на github, т. к. в процессе публикации первых статей, были найдены некоторые моменты, которые не были обнаружены при написании изначальной версии 11 лет назад);

          2. Тут я не совсем понял что именно за проблема имеется в виду, т. к. если это dangling if, то в текущим синтаксисе она в принципе не может возникнуть, т. к. {} обязательны после условия (в изначальной версии проблема решалась, по аналогии со многими другими компиляторами — путем привязки else к самому вложенному if).


          1. WASD1
            05.02.2023 15:59

            1. Смысл всего пункта - вопросы по языку принимаются, или это not the point. Вот у вас тип возвращаемого значения можно не указывать, если он void, почему так? Вот у вас let, а не let mut (т.е. по-умолчанияю данные мутабельны), почему так? Вот у вас return следовательно {} не является expression (следовательно switch expression надо будет приделывать к языку отдельным оператором), почему так? Вот вы собираетесь делать вывод типов (вводить auto по-простому), уже думали насколько мощным он может быть в такой системе типизации? ...

              2.1 Изначальная проблема: dangling if (не знал термина) на пустом месте усложняет язык для программиста (т.е. плохой неинтуитивный дизайн). Т.е. программисту надо не только помнить 100500 объективно сложных моментов, а ещё и вдобавок 100500 неоднозначностей, которых при лучшем дизайне не было бы.

              2.2 Извините был невнимателен, действительно примеры нельзя интерпретировать никак иначе, кроме обязательности скобок.


            1. DCSinpo Автор
              05.02.2023 17:58
              +1

              Теперь я вас понял. Как я уже указал в предыдущем комментарии, язык был взят такой, что бы его можно было просто реализовать, поэтому синтаксис на тот момент был взят с C++ (т. к. на тот момент я с ним больше всего работал), за исключением того, что переменные объявлялись в виде:

              var int p;

              это было сделано для того, что бы исключить неоднозначностей вида:

              a * b;

              Что это объявление переменной или выражение?

              Аналогично и для функций:

              def name(int a, int b): int;

              При подготовке к публикации первой статьи я внес некоторые правки и заменил var, на let, а тип нужно теперь указывать после имени переменной (что больше походит на синтаксис Typescript, Rust и F#). Аналогично были убраны обязательные скобки после if, for, while и добавлены скобки {} после этих конструкций для их тел.

              На момент написания изначальной версии я еще не знал про Rust и не работал с функциональными языками, а во многих других языках обычно {} не являются выражениями.

              Для реализации switch нужно решить много различных вопросов, разрешать ли метки в виде диапазонов, нужно ли разрешать конструкции на подобии (https://en.wikipedia.org/wiki/Duff%27s_device) или нет? Поэтому было принято решение взять только базовые конструкции if, for, while.

              Вывод типов не ограничивается только выводом типов для переменных, но и выводом параметров функций, что конечно лучше делать в разрезе создания аналога generic.

              А так да, в дизайне языков достаточно много различных нюансов (не даром их количество превышает количество пальцев на руках), которые не подразумевались решаться в рамках данной серии, т. к. цель была именно в предоставлении инструмента для создания своего языка программирования, под свой синтаксис и свои правила. Т.к. все статьи, которые я видел в основном заканчивались созданием простого языка вида Kaleidoscope (Если есть что-то более серьезное, то было бы интересно посмотреть).


              1. WASD1
                05.02.2023 18:22
                +1

                Да спасибо понял.

                Не буду писать дальше, чтобы не быть похожим на ворчливого деда ))
                Разумеется написать компилятор к боле-менее взрослому языку крайне объёмная задача и лучше работающий прототип, чем идеальная модель без единой строчки кода.

                Удачи вам.


                1. DCSinpo Автор
                  05.02.2023 19:02
                  +1

                  Спасибо, за интересные вопросы. Я думаю тем, кто будет создавать свой собственный полноценный язык программирования, наша дискуссия пригодится).

                  На всякий случай скину пару ссылок для тех, кому это будет интересно:

                  1. Concepts of Programming Languages

                  2. Programming Language Concepts

                  3. Programming Language Design and Implementation

                  4. Programming Language Pragmatics


                  1. WASD1
                    05.02.2023 19:20
                    +1

                    Спасибо классные ссылки.
                    Вот ещё, must see на мой взгляд (цикл лекций) - просто такого уровня изложение я мало где видел.

                    https://www.youtube.com/watch?v=QOIn8Uh3lkE


                  1. WASD1
                    05.02.2023 21:42

                    А, к стати если уж речь зашла об этом - вы какие из этих книг читали и можете посоветовать?
                    Судя по отзывам - у них у всех есть один большой недостаток - их писали люди из университетов, а не индустрии. И поэтому они сосредоточены на том, как делать "правильно в сферическом вакууме", а не как делать так, чтобы потом на этом было удобно программировать.

                    А вот почитать захотелось на эту тему что-то:
                    1. боле-менее фундаментальное
                    2. боле-менее с практической стороны
                    а нету. Лекция что я привёл, соответствует только критерию (2)


                    1. DCSinpo Автор
                      05.02.2023 23:41
                      +1

                      Это было давно, но я читал Programming Language Pragmatics и не всю, а интересующие меня части (например пропустил все, что дублировало информацию из книги Compilers: Principles, Techniques, and Tools). Но при желании все эти книги можно найти и пролистать, перед прочтением. Для себя я их отложил.


  1. Mmv85
    05.02.2023 09:35

    Работаю сейчас в этой теме. Фронтенд в целом не интересует, подожду статьи про оптимизацию и тбаа


    1. DCSinpo Автор
      05.02.2023 09:41

      Данная серия направленна на то, что бы дать представление о том, как написать свой язык высокого уровня и как различные высокоуровневые конструкции могут быть реализованы при использовании LLVM IR. Поэтому интересующие вас вопросы, в них затрагиваться не будут.


    1. WASD1
      05.02.2023 15:23

      А просвятите пожалуйста (если серьёзно работаете):
      1. TBAA же фронтэнд (делается на call-graph, то есть до IR), разве нет, т.е. как понимать вашу фразу "форнтэнд не интересует, интересует TBAA"?

      2. А разве с C\C++ подобных языках чего-то ждут от TBAA?
      Насколько я понял, что консенсус:
      - не только алгоритмически неразрешимо, но и каких-то прорывных подходов ждать не стоит => если за N^1.5 (условно) не удалось доказать эквивалентность всё считаем разным.
      - ну и strict aliasing для тех, кто ССЗБ.


      1. Mmv85
        05.02.2023 16:08

        Да я в принципе не сильно понимаю, как работает тбаа, и есть ли от него серьезный профит. Если это условные +10% к производительности, то заниматься не будем

        Языков у нас много и работа в целом заключается в маппинге проприетарного IR на LLVM IR. До фронтенда там добраться довольно сложно. Если интересно, есть старая презентация https://llvm.org/devmtg/2017-10/slides/Reagan-Porting OpenVMS Using LLVM.pdf


        1. WASD1
          05.02.2023 16:39

          Я не совсем понял что значит "проприетарный IR" (просто тут может быть условно и JIT-объектник (который почти не отличается от исходного кода), а может быть уже почти готовый бинарнит (этот вариант намного хуже). Если "проприетарный IR" похож на бинарник (например появились архитектурно-специфичные команды, есть конкретный флаговый регистр, виртуальные регистры распределены....) то задача похожа на бинарную трансляцию.

          Если это именно только-только сгенерированный IR - то можете дальше не читать.

          ====================
          Если задача похожа на бинарную трансляцию, то:

          1. Дооптимизировать код, лежащий в бинарнике "generally" идея так себе. Сделать бы так, чтобы работало (что не отменяет того факта, что если у вас изначально очень слабый компилятор - можно иногда что-то и улучшить).

          2. Делать TBAA на IR это боль. Слишком много паразитной семантики в бинарник уже внесли (люди по незнанию пишут "слишком мало семантики в бинарнике" - это обратно реальному положению дел)

          3. Один из известных подходов - делать отдельную фазу компиляции RTMD (run time memory delimitation), которая в run-time проверяет пересекаются ли два "массива" (указатель и отдельный атрибут длины) - если удалось доказать, что не пересекаются, выполняют оптимизированный код, если нет (проверка консервативна) - код предусматривающий возможность пересечения.

          Как под это дело приспособить LLVM и можно ли (как бы он ещё свою паразитную семантику не внёс, свзязанную с ограничениями LLVM-IR, надо проверять), я не знаю.


  1. san-smith
    05.02.2023 14:32

    Спасибо за этот цикл статей, с интересом жду продолжения.
    С меня плюсик в статью и карму:)


  1. Guul
    05.02.2023 18:08
    +1

    Если честно, не понял почему intExprAst::isTrue всегда true, а floatExprAst - не всегда.


    1. DCSinpo Автор
      05.02.2023 18:48

      Это ошибка. Спасибо, исправил.