Recursive Descent Parsing 101

Table of Contents

1. Introduction

  • I show the steps I've followed to write a recursive descent parser in C for expressions in the simple arithmetic language defined by this grammar:

    Exp     ::= Term {("+"|"-") Term}
    Term    ::= Factor {("*"|"/") Factor}
    Factor  ::= ("+"|"-") Factor
              | Primary
    Primary ::= "(" Exp ")"
              | NumLiteral
    
  • The code is here: https://codeberg.org/pietroiusti/arit-parser/commits/branch/blog_post.

1.1. Prerequisites

  • Roughly, you should have an idea about what the following things are:
    • tokenizer (aka scanner);
    • parser;
    • difference between a top-down and a bottom-up parser;
    • grammar;
    • what it means for a grammar to be ambiguous;
    • syntax;
    • microsyntax;
    • E/BNF;
    • AST;
    • CST;
    • precedence;
    • associativity.

2. First sketch

(6b0e9a55bd)

  • Let's start with a simpler grammar: let our language be made of the three a, b, c variables and the multiplication operator only.
  • The following is a natural way to write the grammar for such a language in BNF:

    Exp ::= Exp * Exp | Id
    Id  ::= "a" | "b" | "c"
    
  • This grammar, though, is ambiguous.
  • We could rewrite it thus (Cf. Dragon Book p. 199):

    Exp  ::= Exp * Id | Id
    Id   ::= "a" | "b" | "c"
    
  • This grammar is not ambiguous. But there's still a problem: it is left recursive. Since it is left recursive, we cannot use a top-down parsing in general and recursive descent in particular.
  • We can get rid of the left-recursion by translating the grammar into a right-recursive one (The Dragon Book, among others, explains how):

    Exp  ::= Id Exp'
    Exp' ::= "*" Id Exp' | Eps
    Id   ::= "a" | "b" | "c"
    
  • Eps here stands for "Epsilon" which means "nothing".
  • Okay. Let's see some code.
  • parser.c is our main file.
  • main takes the user input, initializes the scanner, and starts the parsing by calling parse.
  • I'm not going to explain how the scanner works. It is based on the scanner shown in Crafting Interpreters. Check that out. The parser uses next_token to pull tokens from scanner until the input is entirely consumed.
  • Each rule in the grammar has a corresponding function in parser.c:
    • parse_exp;
    • parse_exp_prime;
    • parse_id.
  • parse pulls the first token and expects it to be an Exp. Accordingly, it calls parse_exp in the attempt to match an Exp. After an Exp is successfully parsed, the input is expected to have been entirely consumed, so parse returns parse_exp's return value — which contains the parse tree.
  • If something goes wrong during the parsing process, then an error is printed and exit is called.
  • parse_exp corresponds to the first rule in our grammar (Exp ::= Id Exp').
    • The code is self-explanatory:
      • i) we parse an Id,
      • ii) we parse an Exp',
      • we return a node which represents the Exp whose two only children are the nodes returned in i) and ii).
  • The other parsing functions work analogously.

2.1. parser.h

#ifndef arit_pars_h
#define arit_pars_h

typedef enum {
    NODE_ID,
    NODE_EXP,
    NODE_EXPPRIME,
    NODE_EPS, // ε
} NodeType;

typedef struct ASTNode {
    NodeType type;
    union {
        char id;
        struct {
            struct ASTNode *left;
            struct ASTNode *right;
        } binary;
    } data;
} ASTNode;

#endif

2.2. parser.c

#include <stdio.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <assert.h>
#include <stdlib.h>
#include "parser.h"
#include "scanner.h"
#include "printer.h"

typedef struct {
    Token current;
} Parser;

static Parser parser;

static ASTNode *make_id(Token id);
static ASTNode *make_exp(ASTNode *left, ASTNode *right);
static ASTNode *make_exp_prime(ASTNode *left, ASTNode *right);
static ASTNode *make_eps();
static ASTNode *parse(Scanner *scanner);
static ASTNode *parse_id(Scanner *scanner);
static ASTNode *parse_exp(Scanner *scanner);
static ASTNode *parse_exp_prime(Scanner *scanner);

/* start parsing process */
static ASTNode *parse(Scanner *scanner) {
    parser.current = next_token(scanner); // fetch the first token
    ASTNode *exp = parse_exp(scanner);
    if (parser.current.type != TOKEN_EOF) {
        fprintf(stderr, "unexpected token after expression\n");
        exit(1);
    }
    return exp;
}

/* ======= node constructors start =================================== */
static ASTNode *make_id(Token id) {
    ASTNode *node = malloc(sizeof(ASTNode));
    node->type = NODE_ID;
    node->data.id = *id.start;
    return node;
}

static ASTNode *make_exp(ASTNode *left, ASTNode *right) {
    ASTNode *node = malloc(sizeof(ASTNode));
    node->type = NODE_EXP;
    node->data.binary.left = left;
    node->data.binary.right = right;
    return node;
}

static ASTNode *make_exp_prime(ASTNode *left, ASTNode *right) {
    ASTNode *node = malloc(sizeof(ASTNode));
    node->type = NODE_EXPPRIME;
    node->data.binary.left = left;
    node->data.binary.right = right;
    return node;
}

static ASTNode *make_eps() {
    ASTNode *node = malloc(sizeof(ASTNode));
    node->type = NODE_EPS;
    return node;
}
/* ======= node constructors end ===================================== */

/* ======= parsing functions start =================================== */
// Exp  ::= Id Exp'
static ASTNode *parse_exp(Scanner *scanner) {
    ASTNode *id = parse_id(scanner);
    return make_exp(id, parse_exp_prime(scanner));
}

// Id   ::= "a" | "b" | "c"
static ASTNode *parse_id(Scanner *scanner) {
    if (parser.current.type == TOKEN_ID) {
        Token id_token = parser.current;
        parser.current = next_token(scanner);
        return make_id(id_token);
    } else {
        fprintf(stderr, "parse_id failed\n");
        exit(1);
    }
}

// Exp' ::= "*" Id Exp' | Eps
static ASTNode *parse_exp_prime(Scanner *scanner) {
    if (parser.current.type == TOKEN_STAR) {
        parser.current = next_token(scanner); // consume star
        ASTNode *id = parse_id(scanner);
        ASTNode *ep = parse_exp_prime(scanner);
        return make_exp_prime(id, ep);
    } else {
        return make_eps();
    }
}
/* ======= parsing functions end ===================================== */

int main(int argc, char **argv) {
    if (argc != 2) {
        printf("Usage: arit \"(a+b)*c\"");
        return 1;
    }

    Scanner scanner;

    init_scanner(&scanner, argv[1]);

    ASTNode *root = parse(&scanner);

    printf("Complete parse successful!\n");
    print_ast(root, 0);

    return 0;
}

2.3. scanner.h

#ifndef arit_scan_h
#define arit_scan_h

typedef struct {
    const char *start;
    const char *current;
    int line;
} Scanner;

typedef enum {
    TOKEN_STAR,
    TOKEN_ID,
    TOKEN_ERROR,
    TOKEN_EOF,
} TokenType;

typedef struct {
    TokenType type;
    const char *start;
    int length;
    int line;
} Token;

void init_scanner(Scanner *scanner, const char*);
Token next_token(Scanner *scanner);

#endif

2.4. scanner.c

#include <string.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <assert.h>
#include "scanner.h"

static bool is_at_end(Scanner *scanner);
static char advance(Scanner *scanner);
static Token make_token(Scanner *scanner, TokenType);
static Token make_error_token(Scanner *scanner, const char*);
static void skip_space(Scanner *scanner);
static bool match_space(Scanner *scanner);

static char advance(Scanner *scanner) {
    scanner->current++;
    return scanner->current[-1];
}

static bool is_at_end(Scanner *scanner) {
    return *scanner->current == '\0';
}

static void skip_space(Scanner *scanner) {
    while (match_space(scanner)) ;
}

static bool match_space(Scanner *scanner) {
    if (is_at_end(scanner)) return false;
    if (*scanner->current == ' ' || *scanner->current == '\t') {
        scanner->current++;
        return true;
    }
    if (*scanner->current == '\n') {
        scanner->line++;
        scanner->current++;
        return true;
    }
    return false;
}

static Token make_token(Scanner *scanner, TokenType type) {
    Token token;
    token.type = type;
    token.start = scanner->start;
    token.length = (int)(scanner->current - scanner->start);
    token.line = scanner->line;
    return token;
}

static Token make_error_token(Scanner *scanner, const char* message) {
    Token token;
    token.type = TOKEN_ERROR;
    token.start = message;
    token.length = (int)strlen(message);
    token.line = scanner->line;
    return token;
}

void init_scanner(Scanner *scanner, const char* source) {
    scanner->start = source;
    scanner->current = source;
    scanner->line = 1;
}

Token next_token(Scanner *scanner) {
    skip_space(scanner);
    scanner->start = scanner->current;
    if (is_at_end(scanner)) return make_token(scanner, TOKEN_EOF);
    char c = advance(scanner);
    switch(c) {
    case '*':
        return make_token(scanner, TOKEN_STAR);
    case 'a':
    case 'b':
    case 'c':
        return make_token(scanner, TOKEN_ID);
    }
    return make_error_token(scanner, "Unexpected character.");
}

2.5. printer.h

#ifndef arit_print_h
#define arit_print_h

#include "parser.h"

void print_ast(ASTNode *node, int indent);

#endif

2.6. printer.c

#include "printer.h"
#include <stdio.h>

static const char* node_type_to_str(NodeType type) {
    switch (type) {
    case NODE_ID:          return "ID";
    case NODE_EXP:         return "EXP";
    case NODE_EXPPRIME:    return "EXP_PRIME";
    default:               return "UNKNOWN";
    }
}

static void print_indent(int indent) {
    for (int i = 0; i < indent; i++)
        printf("  ");
}

void print_ast(ASTNode *node, int indent) {
    if (node->type == NODE_EPS) {
        print_indent(indent);
        printf("(Epsilon)\n");
        return;
    }

    print_indent(indent);
    printf("%s", node_type_to_str(node->type));

    switch (node->type) {
    case NODE_ID:
        printf(": %c\n", node->data.id);
        break;
    case NODE_EXP:
    case NODE_EXPPRIME:
        printf("\n");
        print_ast(node->data.binary.left, indent + 1);
        print_ast(node->data.binary.right, indent + 1);
        break;
    default:
        printf("\n");
        break;
    }
}

2.7. Let's try it

$ gcc -W -Wall -O2 -g *.c -o parser
$ ./parser a
Complete parse successful!
EXP
  ID: a
  (Epsilon)
$ ./parser a*b*c
Complete parse successful!
EXP
  ID: a
  EXP_PRIME
    ID: b
    EXP_PRIME
      ID: c
      (Epsilon)
  • Looks good. But we have several things to do.
    1. Our parser currently constructs a so-called "parse tree", aka "Concrete Syntax Tree", as opposed to an AST. We want to build an AST instead.
    2. When we replaced left-recursion with right-recursion we solved one problem, but created a new one: our multiplication operator is now right asssociative. Multiplication — and addition, subtraction, and division too — should be left associative.
    3. We need to be able to parse additions (e.g., 1+2), subtractions (e.g., 1-2), divisions (e.g., 1/2), as well as groups delimited by parentheses (e.g., (1+2)*3).

3. Construct an AST, not a CST

(1acb41c037)

  • Let's replace the construction of a parse tree with the construction of an AST.
  • Currently, parsing a * b constructs this tree:

        exp
        /\
      Id  Exp'
      /   / | \
     /   /  |  \
    a  "*"  Id  Exp'
            |    |
            b   Eps
    
  • The printer prints this (the * node is implicit):

    EXP
      ID: a
      EXP_PRIME
        ID: b
        (Epsilon)
    
  • We want the parser to construct the following, simpler, tree:

      mul
      /\
     /  \
    Id  Id
    |   |
    a   b
    
  • Accordingly, we want the printer to print this:

    MUL
      ID: a
      ID: b
    
  • In parser.c:
    • Get rid of make_exp, make_exp_prime, and make_eps. We don't need those kinds of nodes anymore.
    • Modify make_id so that it simply takes a char now:

      static ASTNode *make_id(char id) {
          ASTNode *node = malloc(sizeof(ASTNode));
          node->type = NODE_ID;
          node->data.id = id;
          return node;
      }
      
    • Add a make_mul function:

      static ASTNode *make_mul(ASTNode *left, ASTNode *right) {
          ASTNode *node = malloc(sizeof(ASTNode));
          node->type = NODE_MUL;
          node->data.binary.left = left;
          node->data.binary.right = right;
          return node;
      }
      
    • modify parse_exp so that it now returns either an Id node or a Mul node::

      static ASTNode *parse_exp(Scanner *scanner) {
          ASTNode *id = parse_id(scanner);
          ASTNode *ep = parse_exp_prime(scanner);
      
          if (!ep) return make_id(id->data.id);
      
          if (ep->type == NODE_ID || ep->type == NODE_MUL) {
              return make_mul(id, ep);
          }
      
          fprintf(stderr, "unrecognized type\n");
          exit(1);
      }
      
    • modify parse_id. It has to pass a char to make_id:

      static ASTNode *parse_id(Scanner *scanner) {
          if (parser.current.type == TOKEN_ID) {
              Token id_token = parser.current;
              parser.current = next_token(scanner);
              return make_id(*id_token.start);
          } else {
              fprintf(stderr, "parse_id failed\n");
              exit(1);
          }
      }
      
    • modify parse_exp_prime so that it returns either an Id node, or a Mul node, or NULL.

      static ASTNode *parse_exp_prime(Scanner *scanner) {
          if (parser.current.type == TOKEN_STAR) {
              parser.current = next_token(scanner); // consume star
              ASTNode *id = parse_id(scanner);
              ASTNode *ep = parse_exp_prime(scanner);
              if (!ep) return make_id(id->data.id);
              return make_mul(id, ep);
          } else {
              return NULL;
          }
      }
      
  • In parser.h:
    • in NodeType get rid of NODE_EXP, NODE_EXPPRIME, NODE_EPS, and add NODE_MUL :

      typedef enum {
          NODE_ID,
          NODE_MUL,
      } NodeType;
      
  • In printer.c:
    • modify node_type_to_str and print_ast accordingly:

      static const char* node_type_to_str(NodeType type) {
          switch (type) {
          case NODE_ID:          return "ID";
          case NODE_MUL:         return "MUL";
          default:               return "UNKNOWN";
          }
      }
      
      void print_ast(ASTNode *node, int indent) {
          print_indent(indent);
          printf("%s", node_type_to_str(node->type));
      
          switch (node->type) {
          case NODE_ID:
              printf(": %c\n", node->data.id);
              break;
          case NODE_MUL:
              printf("\n");
              print_ast(node->data.binary.left, indent + 1);
              print_ast(node->data.binary.right, indent + 1);
              break;
          default:
              printf("\n");
              break;
          }
      }
      

3.1. Let's try it

$ ./parser a*b
Complete parse successful!
MUL
  ID: a
  ID: b

4. Make operator left associative

(b5cb41230)

  • Let's now replace the right recursion with a loop in order to construct the tree in a way such that the multiplication operator is left associative as opposed to right associative.
  • Using EBNF is more natural than BNF now that we use iteration as opposed to recursion:
    • Instead of:

      Exp  ::= Id Exp'
      Exp' ::= "*" Id Exp' | Eps
      
    • Simply:

      Exp  ::= Id {"*" Id}
      

      where the braces stands for the the Kleene closure.

  • The printed tree for a*b*c is currently this:

    MUL
      ID: a
      MUL
        ID: b
        ID: c
    
  • We want this:

    MUL
      MUL
        ID: a
        ID: b
      ID: c
    
  • In parser.c:
    • get rid of parse_exp_prime.
    • Modify parse_exp by introducing a loop that allows us to build the tree in the direction we want:

      static ASTNode *parse_exp(Scanner *scanner) {
          // Ret val is either an id node or a mul node
          ASTNode *ret = parse_id(scanner);
      
          while (parser.current.type == TOKEN_STAR) {
              parser.current = next_token(scanner); // consume star
              ASTNode *id = parse_id(scanner);
              ret = make_mul(ret, id);
          }
      
          return ret;
      }
      
  • We should have a parser working the intended way now.

4.1. Let's try it

$ ./parser a*b*c
Complete parse successful!
MUL
  MUL
    ID: a
    ID: b
  ID: c

5. Handle addition

(0a139718b3)

  • Let's handle addition.
  • The new grammar:

    Exp  ::= Term {"+" Term}
    Term ::= Id {"*" Id}
    
  • In parser.c:
    • Instead of adding a make_add function (which would look almost the same as make_mul), get rid of make_mul and create a higher abstraction to make binary nodes of any type — it'll come handy later:

      static ASTNode *make_binary(NodeType type, ASTNode *left, ASTNode *right) {
          ASTNode *node = malloc(sizeof(ASTNode));
          node->type = type;
          node->data.binary.left = left;
          node->data.binary.right = right;
          return node;
      }
      
    • Since the Exp rule changed, modify parse_exp accordingly:

      static ASTNode *parse_exp(Scanner *scanner) {
          // Ret val is either an id node or an add node
          ASTNode *ret = parse_term(scanner);
      
          while (parser.current.type == TOKEN_PLUS) {
              parser.current = next_token(scanner); // consume plus
              ASTNode *term = parse_term(scanner);
              ret = make_binary(NODE_ADD, ret, term);
          }
      
          return ret;
      }
      
    • Add a parse_term function (the body is the same as the old parse_exp's body, except that now we use make_binary instead of make_mul):

      static ASTNode *parse_term(Scanner *scanner) {
          // Ret val is either an id node or a mul node
          ASTNode *ret = parse_id(scanner);
      
          while (parser.current.type == TOKEN_STAR) {
              parser.current = next_token(scanner); // consume star
              ASTNode *id = parse_id(scanner);
              ret = make_binary(NODE_MUL, ret, id);
          }
      
          return ret;
      }
      
  • In parser.h:

    • add NODE_ADD to NodeType:
    typedef enum {
        NODE_ID,
        NODE_MUL,
        NODE_ADD,
    } NodeType;
    
  • In scanner.h:

    • Add TOKEN_PLUS to TokenType:
    typedef enum {
        TOKEN_PLUS,
        TOKEN_STAR,
        TOKEN_ID,
        TOKEN_ERROR,
        TOKEN_EOF,
    } TokenType;
    
  • in scanner.c:
    • Add a case '+' in the switch in next_token:

      Token next_token(Scanner *scanner) {
          //...
          case '+':
              return make_token(scanner, TOKEN_PLUS);
          //...
      }
      
  • In printer.c:
    • add a case NODE_ADD in node_type_to_str and print_ast:

      static const char* node_type_to_str(NodeType type) {
          switch (type) {
          case NODE_ID:          return "ID";
          case NODE_ADD:         return "ADD";
          case NODE_MUL:         return "MUL";
          default:               return "UNKNOWN";
          }
      }
      
      void print_ast(ASTNode *node, int indent) {
          print_indent(indent);
          printf("%s", node_type_to_str(node->type));
      
          switch (node->type) {
          case NODE_ID:
              printf(": %c\n", node->data.id);
              break;
          case NODE_ADD:
          case NODE_MUL:
              printf("\n");
              print_ast(node->data.binary.left, indent + 1);
              print_ast(node->data.binary.right, indent + 1);
              break;
          default:
              printf("\n");
              break;
          }
      }
      

5.1. Let's try it

$ ./parser a+b*c
Complete parse successful!
ADD
  ID: a
  MUL
    ID: b
    ID: c
$ ./parser a*b+c
Complete parse successful!
ADD
  MUL
    ID: a
    ID: b
  ID: c
  • Notice: operator precedence works the right way.

6. Handle subtraction

(4a48fe4ad9)

  • Let's handle subtraction.
  • In parser.c:
    • Modify the Exp rule in the grammar:

      Exp ::= Term {("+"|"-") Term}
      
    • modify parse_exp:

      static ASTNode *parse_exp(Scanner *scanner) {
          // Ret val is either an id node or an add node
          ASTNode *ret = parse_term(scanner);
      
          TokenType op;
          while (parser.current.type == TOKEN_PLUS || parser.current.type == TOKEN_MINUS) {
              op = parser.current.type; // store op before consuming it
              parser.current = next_token(scanner); // consume the (plus|minus)
              ASTNode *term = parse_term(scanner);
              ret = make_binary(op == TOKEN_PLUS ? NODE_ADD : NODE_SUB, ret, term);
          }
      
          return ret;
      }
      
  • In parser.h:
    • add NODE_SUB to NodeType:

      typedef enum {
          // ...
          NODE_SUB,
      } NodeType;
      
  • In scanner.c:
    • Handle the '-' in scan_token:

      Token next_token(Scanner *scanner) {
          //...
          case '-':
              return make_token(scanner, TOKEN_MINUS);
          //...
      }
      
  • In scanner.h:
    • Add TOKEN_MINUS:

      typedef enum {
          // ...
          TOKEN_MINUS,
          // ...
      } TokenType;
      
  • In printer.c:
    • Handle NODE_SUB in node_type_to_str and print_ast:

      static const char* node_type_to_str(NodeType type) {
          switch (type) {
          // ...
          case NODE_SUB:         return "SUB";
          // ...
          }
      }
      
      void print_ast(ASTNode *node, int indent) {
          // ...
          case NODE_ADD:
          case NODE_SUB:
          case NODE_MUL:
              printf("\n");
              print_ast(node->data.binary.left, indent + 1);
              print_ast(node->data.binary.right, indent + 1);
              break;
          // ...
      }
      

6.1. Let's try it

$ ./parser a*b-c
Complete parse successful!
SUB
  MUL
    ID: a
    ID: b
  ID: c
$ ./parser a-b*c
Complete parse successful!
SUB
  ID: a
  MUL
    ID: b
    ID: c

7. Handle division

(ab74ea004e)

  • Let's now handle division.
  • In parser.c:
    • Update the Term rule in the grammar:

      Term ::= Id {("*"|"/") Id}
      
    • Modify parse_term:

      static ASTNode *parse_term(Scanner *scanner) {
          // Ret val is either an id node or a mul node
          ASTNode *ret = parse_id(scanner);
      
          TokenType op;
          while (parser.current.type == TOKEN_STAR || parser.current.type == TOKEN_SLASH) {
              op = parser.current.type; // store op before consuming it
              parser.current = next_token(scanner); // consume the (star|slash)
              ASTNode *id = parse_id(scanner);
              ret = make_binary(op == TOKEN_STAR ? NODE_MUL : NODE_DIV, ret, id);
          }
      
          return ret;
      }
      
  • In parser.h:
    • Add a NODE_DIV:

      typedef enum {
          // ...
          NODE_DIV,
          // ...
      } NodeType;
      
  • In scanner.c:
    • Handle '/':

      Token scan_token() {
          // ...
          case '/':
              return make_token(TOKEN_SLASH);
          // ...
      }
      
  • In scanner.h:
    • Add a TOKEN_SLASH:

      typedef enum {
          // ...
          TOKEN_SLASH,
          // ...
      } TokenType;
      
  • In printer.c:
    • Handle NODE_DIV in node_type_to_str and print_ast:

      static const char* node_type_to_str(NodeType type) {
          switch (type) {
          // ...
          case NODE_DIV:         return "DIV";
          // ...
          default:               return "UNKNOWN";
          }
      }
      
      void print_ast(ASTNode *node, int indent) {
          // ...
          case NODE_ADD:
          case NODE_SUB:
          case NODE_MUL:
          case NODE_DIV:
              printf("\n");
              print_ast(node->data.binary.left, indent + 1);
              print_ast(node->data.binary.right, indent + 1);
              break;
          // ....
          }
      }
      

7.1. Let's try it

$ ./parser a/b-c
Complete parse successful!
SUB
  DIV
    ID: a
    ID: b
  ID: c
$ ./parser a-b/c
Complete parse successful!
SUB
  ID: a
  DIV
    ID: b
    ID: c

8. Handle groups

(e85ca9a034)

  • In parser.c:
    • Modify the Term rule and add a Factor rule:

      Exp    ::= Term {("+"|"-") Term}
      Term   ::= Factor {("*"|"/") Factor}
      Factor ::= "(" Exp ")"
               | Id
      
    • The Term rule has changed. Modify parse_term accordingly:

      static ASTNode *parse_term(Scanner *scanner) {
          // Ret val is either an id node or a mul node
          ASTNode *ret = parse_factor(scanner);
      
          TokenType op;
          while (parser.current.type == TOKEN_STAR || parser.current.type == TOKEN_SLASH) {
              op = parser.current.type; // store op before consuming it
              parser.current = next_token(scanner); // consume the (star|slash)
              ASTNode *factor = parse_factor(scanner);
              ret = make_binary(op == TOKEN_STAR ? NODE_MUL : NODE_DIV, ret, factor);
          }
      
          return ret;
      }
      
    • Add parse_factor:

      static ASTNode *parse_factor(Scanner *scanner) {
          if (parser.current.type == TOKEN_OPEN_PAREN) {
              parser.current = next_token(scanner); // consume '('
              ASTNode *exp = parse_exp(scanner);
              if (parser.current.type != TOKEN_CLOSE_PAREN) {
                  fprintf(stderr, "expected ')'\n");
                  exit(1);
              }
              parser.current = next_token(scanner); // consume ')'
              return exp;
          }
          return parse_id(scanner);
      }
      
  • In scanner.c:
    • handle ( and ) in scanToken:

      Token scan_token() {
          // ...
          case '(':
              return make_token(TOKEN_OPEN_PAREN);
          case ')':
              return make_token(TOKEN_CLOSE_PAREN);
          // ...
      }
      
  • In scanner.h:
    • Add TOKEN_OPEN_PAREN and TOKEN_CLOSE_PAREN in TokenType:

      typedef enum {
          // ...
          TOKEN_OPEN_PAREN,
          TOKEN_CLOSE_PAREN,
          // ...
      } TokenType;
      

8.1. Let's try it

$ ./parser '((((a))))'
Complete parse successful!
ID: a
$ ./parser '(a+b)*c'
Complete parse successful!
MUL
  ADD
    ID: a
    ID: b
  ID: c

9. Handle number literals

(719c6f39a1)

  • Let's get rid of the a, b, c variables and use numbers instead, so that we can parse expressions like 1+22*333.
  • Replace Id with NumLiteral in the Factor rule:

    Factor ::= "(" Exp ")"
             | NumLiteral
    
  • In parser.c:
    • Get rid of make_id.
    • Add make_num_literal:

      static ASTNode *make_num_literal(const char *s, int length) {
          ASTNode *node = malloc(sizeof(ASTNode));
          node->type = NODE_NUM_LITERAL;
          node->data.num_literal.s = s;
          node->data.num_literal.length = length;
          return node;
      }
      
    • Adjust the comments in parse_exp and parse_term:

      static ASTNode *parse_exp(Scanner *scanner) {
        // Ret val is either a num literal node or an add node
        // ...
      }
      
      static ASTNode *parse_term(Scanner *scanner) {
        // Ret val is either a num literal node or a mul node
        // ...
      }
      
    • Replace the call to parse_id with a call to parse_num in parse_factor:

      static ASTNode *parse_factor(Scanner *scanner) {
          // ...
          return parse_num_literal(scanner);
      }
      
    • Get rid of parse_id.
    • Add parse_num_literal:

      static ASTNode *parse_num_literal(Scanner *scanner) {
          if (parser.current.type == TOKEN_NUM_LITERAL) {
              Token num_literal_token = parser.current;
              parser.current = next_token(scanner);
              return make_num_literal(num_literal_token.start, num_literal_token.length);
          } else {
              fprintf(stderr, "parse_num_literal failed\n");
              exit(1);
          }
      }
      
  • In parser.h:
    • Add a NODE_NUM_LITERAL in ~NodeType~:

      typedef enum {
          NODE_NUM_LITERAL,
          // ...
      } NodeType;
      
    • Update the data union in ASTNode, by getting rid of id and adding num_literal:

      typedef struct ASTNode {
          NodeType type;
          union {
              struct {
                  const char *s;
                  int length;
              } num_literal;
              struct {
                  struct ASTNode *left;
                  struct ASTNode *right;
              } binary;
          } data;
      } ASTNode;
      
  • In scanner.c:
    • We need to add a notion of NumLiteral in the scanner:

      static bool is_digit(char c) {
          return c >= '0' && c <= '9';
      }
      
      // NumLiteral ::= Digit+
      //              | Digit+ "." Digit+
      static bool match_num_literal(Scanner *scanner) {
          if (match_digit(scanner)) {
              while (match_digit(scanner))
                  ;
              if (match(scanner, '.')) {
                  if (match_digit(scanner)) {
                      while (match_digit(scanner))
                          ;
                      return true; // <digit>+ . <digit>+
                  }
                  scanner->current--;
              }
              return true; // <digit>+
          }
          return false;
      }
      
      // Digit ::= 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
      static bool match_digit(Scanner *scanner) {
          if (is_digit(*scanner->current)) {
              advance(scanner);
              return true;
          }
          return false;
      }
      
      static bool match(Scanner *scanner, char expected) {
          if (is_at_end(scanner)) return false;
          if (*scanner->current != expected) return false;
          scanner->current++;
          return true;
      }
      
      • modify next_token by removing the case for 'a', 'b', and 'c' and handling number literals:

        Token next_token(Scanner *scanner) {
            skip_space(scanner);
            scanner->start = scanner->current;
            if (is_at_end(scanner)) return make_token(scanner, TOKEN_EOF);
            if (match_num_literal(scanner)) return make_token(scanner, TOKEN_NUM_LITERAL);
            char c = advance(scanner);
            switch(c) {
            case '+':
                return make_token(scanner, TOKEN_PLUS);
            case '-':
                return make_token(scanner, TOKEN_MINUS);
            case '*':
                return make_token(scanner, TOKEN_STAR);
            case '/':
                return make_token(scanner, TOKEN_SLASH);
            case '(':
                return make_token(scanner, TOKEN_OPEN_PAREN);
            case ')':
                return make_token(scanner, TOKEN_CLOSE_PAREN);
            }
            return make_error_token(scanner, "Unexpected character.");
        }
        
  • In scanner.h:
    • Add TOKEN_NUM_LITERAL in TokenType:

      typedef enum {
          // ...
          TOKEN_NUM_LITERAL,
          // ...
      } TokenType;
      
  • In printer.c:
    • add print_num_literal:

      static void print_num_literal(const char *s, int length) {
          printf(": ");
          for (int i = 0; i < length; i++) {
              printf("%c", *(s+i));
          }
          printf("\n");
      }
      
    • replace the NODE_ID cases in node_type_to_str and print_ast with those for NODE_NUM_LITERAL:

      static const char* node_type_to_str(NodeType type) {
          // ...
          case NODE_NUM_LITERAL:  return "NUM LITERAL";
          // ...
      }
      
      void print_ast(ASTNode *node, int indent) {
          // ...
          case NODE_NUM_LITERAL:
              print_num_literal(node->data.num_literal.s, node->data.num_literal.length);
              break;
          // ...
      }
      

9.1. Let's try it

$ ./parser '(1+22)*333'
Complete parse successful!
MUL
  ADD
    NUM LITERAL: 1
    NUM LITERAL: 22
  NUM LITERAL: 333

10. Handle negation sign

(debf5498c8)

  • Update the grammar byt modifying Factor and adding Primary:

    Exp     ::= Term {("+"|"-") Term}
    Term    ::= Factor {("*"|"/") Factor}
    Factor  ::= "-" Factor
              | Primary
    Primary ::= "(" Exp ")"
              | NumLiteral
    
  • In parser.c:
    • add make_neg:

      static ASTNode *make_neg(ASTNode *negated_node) {
          ASTNode *node = malloc(sizeof(ASTNode));
          node->type = NODE_NEG;
          node->data.unary.node = negated_node;
          return node;
      }
      
    • modify parse_factor:

      static ASTNode *parse_factor(Scanner *scanner) {
          if (parser.current.type == TOKEN_MINUS) {
              parser.current = next_token(scanner); // consume '-'
              ASTNode *factor = parse_factor(scanner);
              return make_neg(factor);
          }
      
          return parse_primary(scanner);
      }
      
    • add parse_primary (which has the same body of our old parse_factor):

      static ASTNode *parse_primary(Scanner *scanner) {
          if (parser.current.type == TOKEN_OPEN_PAREN) {
              parser.current = next_token(scanner); // consume '('
              ASTNode *exp = parse_exp(scanner);
              if (parser.current.type != TOKEN_CLOSE_PAREN) {
                  fprintf(stderr, "expected ')'\n");
                  exit(1);
              }
              parser.current = next_token(scanner); // consume ')'
              return exp;
          }
          return parse_num_literal(scanner);
      }
      
  • In parser.h:
    • add NODE_NEG in NodeType:

      typedef enum {
          NODE_NEG,
          // ...
      } NodeType;
      
    • add a unary member in the data union in ASTNode:

      typedef struct ASTNode {
          // ...
          union {
              // ...
              struct {
                  struct ASTNode *node;
              } unary;
              // ...
          } data;
      } ASTNode;
      
  • In printer.c:
    • Add a NODE_NEG case in node_type_to_str and print_ast:

      static const char* node_type_to_str(NodeType type) {
          //...
          case NODE_NEG:  return "NEG";
          // ...
      }
      
      void print_ast(ASTNode *node, int indent) {
          // ...
          case NODE_NEG:
              printf("\n");
              print_ast(node->data.unary.node, indent + 1);
              break;
          // ...
      }
      

10.1. Let's try it

$ ./parser 1--2
Complete parse successful!
SUB
  NUM LITERAL: 1
  NEG
    NUM LITERAL: 2
$ ./parser 1--------2
Complete parse successful!
SUB
  NUM LITERAL: 1
  NEG
    NEG
      NEG
        NEG
          NEG
            NEG
              NEG
                NUM LITERAL: 2

11. Handle positive sign

(646fd2fce5)

  • New Factor rule:

    Factor  ::= ("+"|"-") Factor
              | Primary
    
  • Modify parse_factor (we don't need to create POS nodes, since +x == x):

    // Factor  ::= ("+"|"-") Factor
    //           | Primary
    static ASTNode *parse_factor(Scanner *scanner) {
        TokenType curr;
        if (parser.current.type == TOKEN_PLUS || parser.current.type == TOKEN_MINUS) {
            curr = parser.current.type;
            parser.current = next_token(scanner); // consume the (plus|minus)
            ASTNode *factor = parse_factor(scanner);
            return curr == TOKEN_PLUS ? factor : make_neg(factor);
        }
    
        return parse_primary(scanner);
    }
    

11.1. Let's try it

$ ./parser +1
Complete parse successful!
NUM LITERAL: 1
$ ./parser +++1+++2
Complete parse successful!
ADD
  NUM LITERAL: 1
  NUM LITERAL: 2
$ ./parser +++1++-2
Complete parse successful!
ADD
  NUM LITERAL: 1
  NEG
    NUM LITERAL: 2
  • We are done!

12. Resources

Send me an email for comments.

Created with Emacs 30.2.50 (Org mode 9.7.11)