В прошлой статье мы реализовывали алгоритм унификации (unification) на Rust-е. Теперь давайте применим его на реальном примере - простое lambda исчисление. Синтаксис выражений у нашего простого lambda calculus-а будет выглядеть так: В данном случае \ <identifier> . <expr> это синтаксис для лямбд: А <expr> <expr>, для применений: Теперь когда с синтаксисом мы ознакомились, мы можем приступить к самому сладкому: реализации! Первое, что нам нужно сделать с исходником на нашем простеньком языке программирования это превратить его в список токенов, то есть токенизировать: Токен - здесь можно рассматривать как грамматическую единицу языка: разные виды пунктуации, название (идентификатор), иногда пробелы, ключевые слова и т.д. Разметим структуру токена в нашем языке: Реализуем токенизацию! Здесь мы обрабатываем токены состоящие из единичных символов - пунктуацию. Теперь самое интересное - идентификаторы: Здесь мы делаем несколько вещей. Во-первых, мы пропускаем пробелы в нашей программе (никакого значения даже с точки зрения синтаксиса они здесь не имеют). Во-вторых, если мы обнаруживаем невалидный символ, мы выводим ошибку (пока-что делаем панику, что позже исправим). В-третьих обрабатываем идентификаторы: Пока следующий символ это либо число, либо буква, либо '_', мы продолжаем идти дальше и добавлять символы в нашу строку name, иначе мы прерываем цикл и идем сканировать следующие токены. Теперь же, когда мы имеем список токенов (с которым работать проще, нежели чем с исходным кодом), мы должны провести синтаксический анализ: Парсер берет список токенов и преобразует его в абстрактное синтаксическое дерево или AST. Первое, что мы должны сделать - разметить AST для выражений: Я надеюсь, здесь вопросов не должно быть.  Теперь мы должны создать структуру, которая будет хранить итератор по токенам, для того, чтобы по ним проходиться: Теперь можно приступить к самому интересному! Начнем с выражений завернутых в скобки: Здесь мы не проверяем на открывающую скобку, потому-что предполагается, что при вызове функции она уже обработана. Теперь обработка лямбд: Здесь тоже предполагается, что \ уже обработан. В данном случае мы можем заметить метод parse_primary_expression. Это по сути парсинг всех выражений кроме применений (<expr> <expr>), потому-что например здесь: Мы парсим не x a цельно, а берем толькоx. И поэтому получаем совершенно другой порядок действий, так как \x.x и a это два раздельных выражения. Реализуем этот самый метод! Здесь мы вызываем все функции, которые мы написали до этого и еще обрабатываем простые переменные.  Теперь же нам нужно обрабатывать применения (<expr> <expr>):  Здесь мы продолжаем анализировать выражения пока не встретим закрывающую скобку или пока не уткнемся в конец входных данных: Ну и самое логичное, мы анализируем аргумент при применении и сохраняем это все в AST: Таким образом мы реализовали простой парсер за ~130 строк кода для lambda исчисления. А теперь то чего мы так долго ждали: Помните в прошлой статье, я говорил про type constraint-ы? Так-вот пока для простоты у нас будет только 1 вид constraint-ов - равенство: То есть как раньше пояснялось, constraint требует, чтобы тип были равны. Соответственно unification будет как раньше принимать левый и правый тип и давать нам новые замены (substitutions). Теперь, так как мы уже начинаем работать со scope-ами, нам нужно где-то хранить информацию о типах. Мы пока-что сделаем это самым очевидным способом:  Теперь создадим контекст для нашего type inference-а: Как видим, здесь есть интересное поле - last_type_variable_index. Нужно оно для генерации новых уникальных переменных типа. Помните? Напишем функции, которые будут генерировать новые переменные типов и сами типы заполнители: А теперь делаем завершающие штрихи. Напишем функцию, которую назовем infer_type. Она будет возвращать тип данного выражения: Первое, что мы должны обработать это самый простой случай. Если выражение - переменная, берем информацию о типе из scope-а. С лямбдами все поинтереснее. Во-первых, мы не знаем тип параметра лямбды. Единственное, что мы можем узнать это ее возвращаемое значение, что мы и делаем: Соответственно, для типа параметра, мы пока оставим переменную типа (которую после можно будет убрать с помощью замен) и конечно же добавляем в текущий scope, чтобы можно было использовать параметр, при анализации возвращаемого значения: Заметим, что мы возвращаем type constructor, с названием Function: В данном случае, первый generic аргумент это тип параметра лямбды, а второй это тип возвращаемого значения. Теперь приступим к анализации применений: Во-первых, мы анализируем типы самой лямбды и аргумента. Возвращаемое значение, мы бы в теории могли бы узнать если бы не типовые переменные. Вместо такого наивного подхода, мы можем пока пометить возвращаемое значение как переменную типа и использовать type constraint: А исходя из информации, что Function<A, ?t> = Function<A, B> мы и так узнаем тип возвращаемого значения (?t = B), а еще и во-вторых проверим типы на type mismatch. Поздравляю, мы почти закончили! Теперь, нам нужна функция, которая будет проходиться по type constraint-ам и применять нашу написанную в прошлой статье функцию unify, чтобы получать замены для типовых переменных: А теперь самое сладкое, проверим работает ли наш алгоритм: Как видим алгоритм понял, что тип выражения \a.(add a a) если add это Function<int, Function<int, int>> это Function<int, int>, что звучит логично: Весь исходный код, который я предоставляю в статьях теперь можно найти здесь. Спасибо большое за внимание! Я следующих статьях, мы будем говорить о более продвинутых способах реализации type inference, интересных фишках и много другого!