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

Что такое семантический анализ?

Семантический анализ отвечает за то, чтобы наши утверждения имели правильное значение, и чтобы указать его, давайте посмотрим, как работает человеческий язык:

Кошка лает.

Это предложение лексически правильное (только допустимые английские слова), синтаксически правильное (все слова в правильном положении), но не хватает детали

Кошка не лает, она мяукает, что означает, что наше предложение бессмысленно, и это проблема, которую решает семантический анализ.

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

5 / "hi"

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

То же самое касается прицелов

number = 32
function fn() {
   number = 2
   return number*2 //should output 4 instead of 64
}

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

number*2 // throws an error because number is not yet defined
number = 32

Также гарантируя, мы всегда используем ранее объявленные переменные.

И запомни:

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

Выполнение семантического анализа в нашем интерпретаторе

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

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

Но сначала…

Нам понадобится новый модуль

lisp-eval/lib/lisp_eval/semantic_analysis.ex

defmodule SemanticAnalysis do  

end

И для этого модуля мы просто определим одну функцию

defmodule SemanticAnalysis do  
   def semantic_analysis(["*" | tail]) do
      {v1, nTail}  = semantic_analysis tail    
      {v2, nNtail} = semantic_analysis nTail    
      {v1 * v2, nNtail}  
   end  
   def semantic_analysis(["/" | tail]) do
      {v1, nTail}  = semantic_analysis tail
      {v2, nNtail} = semantic_analysis nTail    
      case v2 do      
        0 -> raise "Error division by zero"
        _ -> {v1 / v2, nNtail}    
      end
   end
   def semantic_analysis(["-" | tail]) do
     {v1, nTail}  = semantic_analysis tail
     {v2, nNtail} = semantic_analysis nTail
     {v1 - v2, nNtail}
   end
   def semantic_analysis(["+" | tail]) do
     {v1, nTail}  = semantic_analysis tail
     {v2, nNtail} = semantic_analysis nTail
     {v1 + v2, nNtail}
   end
   def semantic_analysis([head | tail]), do:
      {round(String.to_integer(head)), tail}
end

При синтаксическом анализе после обработки нашей программы мы получили примерно следующее:

program = ["*", "5", "5"]

Тогда нам просто нужно позвонить:

SemanticAnalysis.semantic_analysis(program)

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

v1, операция v2

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

И если символ не является оператором Это потому, что это строка, содержащая числовое значение, поэтому мы просто превращаем эту строку в число, а после этого округляем ее, чтобы иметь возможность выполнять деления, которые могут привести к результату с плавающим указателем(пример 3/2 = 1,5)

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

И вуаля наш семантический анализ готов

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

И это понятно, потому что семантический анализ отвечает за смысл программы и является последним шагом перед генерацией кода.

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

Спасибо, что дочитали до этой статьи, и надеемся увидеть вас в следующей статье. Пока!