ПРИМЕЧАНИЕ. Это девятая часть серии руководств Реализация языка программирования в Swift. Обязательно посмотрите предыдущие.

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

  1. Наши функции будут использовать формат function name(parameter) { ... }
    , где function - ключевое слово, представляющее начало определения функции.
  2. Мы просто проигнорируем проблемы, связанные с областью видимости, сделав все переменные глобальными и не давая пользователю возможности затенять существующие.

Итак, с чего начать?

Этот учебник будет очень похож на мои учебники по добавлению поддержки для объявления переменных. Мы сделаем это за четыре простых шага:

  1. Добавьте токены function, {, } и ,.
  2. Добавьте struct FunctionDefinition и выполните рефакторинг нашего глобального identifiers словаря для поддержки как переменных, так и функций.
  3. Добавить метод парсера для FunctionDefinition.
  4. Добавить метод парсера для вызова функции.

Добавление жетонов

Это очень просто. В нашей модели Token мы просто добавляем эти выделенные строки:

enum Token {
    typealias Generator = (String) -> Token?
    case op(Operator)
    case number(Float)
    case identifier(String)
    case parensOpen
    case parensClose
    case `var`
    case equals
    case function
    case curlyOpen
    case curlyClose
    
    case comma
    static var generators: [String: Generator] {
        return [
            "\\*|\\/|\\+|\\-": { .op(Operator(rawValue: $0)!) },
            "\\-?([0-9]*\\.[0-9]+|[0-9]+)": { .number(Float($0)!) },
           "[a-zA-Z_$][a-zA-Z_$0-9]*": { // ADDED - perfected
               guard $0 != "var" else { // ADDED
                   return .var
               }
               guard $0 != "function" else {
                   return .function
               }
               return .identifier($0)
           },
           "\\(": { _ in .parensOpen },
           "\\)": { _ in .parensClose },
           "\\=": { _ in .equals },
           "\\{": { _ in .curlyOpen },
           "\\}": { _ in .curlyClose },
           "\\,": { _ in .comma }
       ]
    }
}

Добавление нашего FunctionDefinition

Это требует небольшого дизайна. Нам нужно задать себе вопрос: «Что такое функция?»

Помните, что мы решили использовать формат function identifier(parameter) { ... }.

Для поддержки этого типа функции мы храним следующее:

  • Идентификатор
  • Параметры
  • Какой-то индикатор того, где начинается и заканчивается наш блок кода.

Прежде чем мы начнем реализовывать наш узел для функций, нам нужно добавить поддержку FunctionDefinition в наш identifiers словарь, который в настоящее время поддерживает только Float значения. Для этого давайте создадим новое перечисление с именем Definition, которое обрабатывает как переменные, так и функции:

enum Definition {
    case variable(value: Float)
    case function(FunctionDefinition)
}

Чтобы исправить ошибки сборки, связанные с отсутствующим типом FunctionDefinition, просто создайте временный:

struct FunctionDefinition: Node {
    // TODO: IMPLEMENT!
    func interpret() throws -> Node {
        return 1 // TODO: IMPLEMENT!
    }
}

Теперь давайте изменим наш identifiers словарь, чтобы использовать новый тип Definition:

var identifiers: [String: Definition] = [
    "PI": .variable(value: Float.pi),
]

При внесении этого изменения вы получите два типа ошибок сборки:

  1. guard let ... = identifiers[...] ... - теперь должно использоваться guard case let .variable(...) = identifiers[...] ....
  2. identifiers[...] = ... - Теперь это должно быть что-то вроде identifiers[...] = .variable(...).

Фиксированный? Здорово! Еще одна вещь: дополнения в этом руководстве потребуют от нас добавить еще две ошибки в наше перечисление Parser.Error:

case invalidParameters(toFunction: String)
case alreadyDefined(String)

Хорошо. Теперь мы готовы реализовать «настоящий» FunctionDefiniton:

struct FunctionDefinition: Node {
    let identifier: String
    let parameters: [String]
    let block: Node
    func interpret() throws -> Float {
        identifiers[identifier] = .function(self)
        return 1 // 1 means SUCCESS
    }
}

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

Функция синтаксического анализа Определение

По сравнению с синтаксическим анализом объявлений переменных в определениях функций есть один хитрый бит - параметры. Чтобы отделить эту логику от нашего parseFunctionDefinition метода, давайте добавим еще один под названием parseParameterList:

func parseParameterList() throws -> [Node] {
    var params: [Node] = []
    guard case .parensOpen = popToken() else {
        throw Error.expected("(")
    }
    while self.canPop {
        guard let value = try? parseValue() else {
            break
        }
        guard case .comma = peek() else {
            params.append(value)
            break
        }
        popToken()
        params.append(value)
    }
    guard canPop, case .parensClose = popToken() else {
        throw Error.expected(")")
    }
    return params
}

Эта функция может показаться немного пугающей, но на самом деле все очень просто:

  1. Разобрать (.
  2. Разберите как можно больше значений, используя parseValue().
  3. Разобрать ).
  4. Верните значения.

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

Достаточно просто? Потрясающий! Давайте добавим нашу parseFunctionDefinition функцию:

func parseFunctionDefinition() throws -> Node {
    guard case .function = popToken() else {
        throw Error.expected("function")
    }
    guard case let .identifier(identifier) = popToken() else {
        throw Error.expectedIdentifier
    }
    let paramNodes = try parseParameterList()
    // Convert the nodes to their String values
    let paramList = try paramNodes
        .map { node -> String in
            guard let string = node as? String else {
                throw Error.expectedIdentifier
            }
            return string
        }
    guard case .curlyOpen = popToken() else {
        throw Error.expected("{")
    }
    let startIndex = index
    while canPop {
        guard case .curlyClose = peek() else {
            index += 1
            continue
        }
        break
    }
    let endIndex = index
    guard case .curlyClose = popToken() else {
        throw Error.expected("}")
    }
    let codeBlock = try Parser(tokens: Array(tokens[startIndex..<endIndex])).parse()
    return FunctionDefinition(identifier: identifier,
                              parameters: paramList,
                              block: codeBlock)
}

Обобщить:

  1. Разобрать function.
  2. Разобрать identifier.
  3. Разобрать список параметров как [String].
  4. Разобрать {.
  5. Теперь мы находимся в начальном индексе блока кода нашей функции. Сохраните это значение как startIndex.
  6. Увеличивайте индекс, пока не найдете закрывающую фигурную скобку.
  7. Теперь мы находимся в конце индекса блока кода нашей функции. Сохраните это значение как endIndex.
  8. Создайте FunctionDefinition и верните его.

Потрясающий!

Теперь единственное, что нужно сделать для синтаксического анализа FunctionDefinition, - это вызвать наш parseFunctionDefinition метод внутри нашего parse метода:

func parse() throws -> Node { // ADDED: New parse method
    var nodes: [Node] = []
    while canPop {
        let token = peek()
        switch token {
        case .var:
            let declaration = try parseVariableDeclaration()
            nodes.append(declaration)
        case .function:
            let definition = try parseFunctionDefinition()
            nodes.append(definition)
        default:
            let expression = try parseExpression()
            nodes.append(expression)
        }
    }
    return Block(nodes: nodes)
}

Вызов функций синтаксического анализа

Перво-наперво! Нам нужен узел для представления вызовов функций:

struct FunctionCall: Node {
    let identifier: String
    let parameters: [Node]
    func interpret() throws -> Float {
        guard let definition = identifiers[identifier],
            case let .function(function) = definition else {

            throw Parser.Error.notDefined(identifier)
        }
        guard function.parameters.count == parameters.count else {
            throw Parser.Error.invalidParameters(toFunction: identifier)
        }
        let paramsAndValues = zip(function.parameters, parameters)
        // Temporarily add parameters to global index
        try paramsAndValues.forEach { (name, node) in

            guard identifiers[name] == nil else {
                throw RuntimeError.alreadyDefined(name)
            }
            identifiers[name] = .variable(value: try node.interpret())
        }
        let returnValue = try function.block.interpret()
        // Remove parameter values from global index after use
        paramsAndValues.forEach { (name, _) in
            identifiers.removeValue(forKey: name)
        }
        return returnValue
    }
}

Помните дизайн / ярлык игнорирования обработки прицела и запрета затенения. FunctionCall - это узел, на котором это происходит.

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

Итак, у нас есть FunctionCall, теперь пора создать метод для его анализа. Это будет очень похоже на то, что мы сделали, когда добавили синтаксический анализ переменных. Давайте перейдем к методу parseValue и заменим предыдущий синтаксический анализ идентификатора следующим выделенным фрагментом:

func parseValue() throws -> Node {
    switch (peek()) {
    case .number:
        return try parseNumber()
    case .parensOpen:
        return try parseParens()
    case .identifier:
        guard let identifier = try parseIdentifier() as? String else {
            throw Error.expectedIdentifier
        }
        guard canPop, case .parensOpen = peek() else {
            return identifier
        }
        let params = try parseParameterList()
        return FunctionCall(identifier: identifier,
                            parameters: params)
    default:
        throw Error.expectedExpression
    }
}

Если .parensOpen следует за .identifier, мы рассматриваем это как вызов функции, в противном случае мы просто возвращаем идентификатор.

Наконец: обновите нашу основную функцию

var code = """
function sum(a, b) {
    a + b
}
var five = 5
var six = 6
sum(five, six)
"""
let tokens = Lexer(code: code).tokens
let node = try Parser(tokens: tokens).parse()
do {
    print(try node.interpret() == 11) // true
} catch {
    print((error as? Parser.Error))
}

Оооочень красиво! 😍

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

function foo(param) {
    param
}
function bar(param) {
    var something = foo(5) Here we get an error as 5 will become foo's "param" but "param" already exists in bar's runtime
    something + param 
}
bar(6)

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

Кстати, на следующей неделе мы полностью доработаем наш язык, добавив поддержку оператора if!

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

P.S. Помните, что вы можете подписаться на меня здесь, на Medium или twitter, чтобы получать уведомления о будущих уроках.