Implementing a Programming Language in Swift — Part 10: If Statements

Þorvaldur Rúnarsson
Swift2Go
Published in
5 min readMar 25, 2019

NOTE: This is the tenth part in a tutorial series on “Implementing a Programming Language in Swift.” Be sure to check out the previous ones. TLDR; Full Source Code

In the previous tutorial we added support for functions. It turns out if statements are very similar.

Like with functions if statements are composed by a keyword and block of code. The difference between the two lies in functions requiring paramaters, while an if statement needs an expression.

If statements add some extra complexity with else and else if but if you’ve been following previous tutorials, this process should be trivial.

You’ve probably asked yourself “How we are going to implement if-statements given that our language only supports a single type?” And this is a valid concern, but for now we will use a truthy/falsy format, interpreting values greater than or equal to one to be truthy and all values below one, falsy.

Adding the Keywords

Like in previous tutorials it all starts with adding the appropriate keywords. This time it’s if and else (we’ve already added brackets “{,” “},” “(” and “)”):

enum Token {    typealias Generator = (String) -> Token?    ...    case comma    case `if`    case `else`    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]*": {                guard $0 != "var" else {                    return .var                }                guard $0 != "function" else {                    return .function                }                guard $0 != "if" else {                    return .if                }                guard $0 != "else" else {                    return .else                }                return .identifier($0)            },            ...        ]    }}

Implementing a Node

Our next step is to add a Node structure for our if statement. This time a little design is involved. We need to ask ourselves “What data should our IfStatement struct include?”

The “if” part includes an expression and a block to be run. “Else-ifs” also include an expression and a block, but “else” doesn’t include an expression.

We could look at an IfStatement as a code block to default to (the “else” block) and a list of tuples, which include a Node (the expression) as well as a start and end for a code block (just like how we stored function code blocks).

The interpret function is also fairly straight forward:

struct IfStatement: Node {    let ifOrIfElse: [(expression: Node, block: Node)]    let block: Node?
func interpret() throws -> Float {
for ifOrElseIf in ifAndIfElse { guard (try ifOrElseIf
.expression.interpret()) >= 1 else {
continue } return try ifOrElseIf.block.interpret() } guard let elseBlock = self.elseBlock else { return -1 } return try elseBlock.interpret() }}

In our interpret method we simply find the first truthy expression and return the interpretation of its associated block. If no expression is found we simply return the value -1.

Parsing IfStatement

Before we start we should add a helper method in our Parser class for parsing code blocks that are within curly brackets. The implementation could be copy pasted from within our function-definition parsing method and goes like this:

func parseCurlyCodeBlock() throws -> Node {    guard canPop, case .curlyOpen = popToken() else {        throw Parser.Error.expected("{")    }    var depth = 1    let startIndex = index    while canPop {        guard case .curlyClose = peek() else {            if case .curlyOpen = peek() {                depth += 1            }            index += 1            continue        }
depth -= 1
guard depth == 0 else {
index += 1

continue
} break } let endIndex = index guard canPop, case .curlyClose = popToken() else { throw Error.expected("}") } let tokens = Array(self.tokens[startIndex..<endIndex]) return try Parser(tokens: tokens).parse()}

We should now also be able to refactor our function-definition parser, but for the sake of this tutorial not wandering off, let’s continue to our IfStatement parser.

This is where I attempt to be clever, so bear with me:

func parseIfStatement() throws -> Node {    guard canPop, case .if = popToken() else {        throw Parser.Error.expected("if")    }    let firstExpression = try parseExpression()
let codeBlock = try parseCurlyCodeBlock() let ifsAndElseIfs: [(Node, Node)] = [ (firstExpression, codeBlock) ] guard canPop, case .else = peek() else { return IfStatement(ifsAndElseIfs: ifsAndElseIfs, elseBlock: nil) } popToken() guard case .if = peek() else { let elseBlock = try parseCurlyCodeBlock() return IfStatement(ifsAndElseIfs: ifsAndElseIfs, elseBlock: elseBlock) } let ifStatement = try parseIfStatement() as! IfStatement return IfStatement(ifsAndElseIfs: ifsAndElseIfs + ifStatement.ifsAndElseIfs, elseBlock: ifStatement.elseBlock)}

This is quite the handful, especially the recursion, if you found it hard to follow I suggest reading up on the subject before coming back to this tutorial. Here’s what’s happening:

  1. We start by parsing the first “If” along with its expression and block.
  2. We check if there is an “else,” if not we simply return a single block IfStatement.
  3. We check if there is also an “if,” if not we interpret the previously found else as the elseBlock. If we do find an “if,” (this is where I attempt to be clever) we attempt to recursively parse an IfStatement.

Great!

Now all left to do is to add IfStatement parsing to our Parser’s main parse method:

func parse() throws -> Node {    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)        case .if:            let statement = try parseIfStatement()            nodes.append(statement)        default:            let expression = try parseExpression()            nodes.append(expression)        }    }    return Block(nodes: nodes)}

Now we are ready to use if statements!

var code = """    var false = 0    var true = 1    if false {        5    } else if true {        6    }"""
let tokens = Lexer(code: code).tokenslet node = try Parser(tokens: tokens).parse()do { print(try node.interpret() == 6) // TRUE} catch { print(error.localizedDescription)}

That’s our language! It’s lacking many necessary features, like proper setting of variables, loops and recursion, to name a few.

It might be shit, but it’s OUR shit and so we won’t let its incompetency drag us down 💪.

Next Steps

Technically we only created a (buggy) frontend of a programming language in this tutorial series. If you’re interested in creating a “real” language, I suggest looking at the following materials:

  • Crafting Interpreters — A FREE online book which walks one through implementing a full blown programming language, virtual machine and everything, it’s low level, so you’ll need to allocate some time for this. I have just started looking at this and so far this book looks like its definitely worth checking out.
  • Compilers: Principles, Techniques and Tools (The Dragon Book) [Affiliate link to Amazon] — One of the more famous books on creating compilers, I can’t say whether it’s the best out there or not, it’s the only one I really looked into, but I certainly found it interesting.

I hope you guys found this tutorial series interesting and as always, don’t be shy to give feedback e.g. by clapping or writing a response.

P.S. Remember that you can follow me here on Medium for notifications on future tutorials.

--

--

Þorvaldur Rúnarsson
Swift2Go

Full Stack Developer (TypeScript, React, Python, Node.js, Swift, ReactNative, Flutter), also... dad, football enthusiast & guitar enthusiast.