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
- Let's start with a simpler grammar: let our language be made of the
three
a,b,cvariables 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"
Epshere stands for "Epsilon" which means "nothing".- Okay. Let's see some code.
parser.cis our main file.maintakes the user input, initializes the scanner, and starts the parsing by callingparse.- 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_tokento 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.
parsepulls the first token and expects it to be anExp. Accordingly, it callsparse_expin the attempt to match anExp. After anExpis successfully parsed, the input is expected to have been entirely consumed, soparsereturnsparse_exp's return value — which contains the parse tree.- If something goes wrong during the parsing process, then an error is
printed and
exitis called. parse_expcorresponds 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
Expwhose two only children are the nodes returned in i) and ii).
- i) we parse an
- The code is self-explanatory:
- 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.
- 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.
- 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.
- 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
- Let's replace the construction of a parse tree with the construction of an AST.
Currently, parsing
a * bconstructs this tree:exp /\ Id Exp' / / | \ / / | \ a "*" Id Exp' | | b EpsThe 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, andmake_eps. We don't need those kinds of nodes anymore. Modify
make_idso that it simply takes acharnow:static ASTNode *make_id(char id) { ASTNode *node = malloc(sizeof(ASTNode)); node->type = NODE_ID; node->data.id = id; return node; }
Add a
make_mulfunction: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_expso that it now returns either anIdnode or aMulnode::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 achartomake_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_primeso that it returns either anIdnode, or aMulnode, orNULL.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; } }
- Get rid of
- In
parser.h:in
NodeTypeget rid ofNODE_EXP,NODE_EXPPRIME,NODE_EPS, and addNODE_MUL:typedef enum { NODE_ID, NODE_MUL, } NodeType;
- In
printer.c:modify
node_type_to_strandprint_astaccordingly: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
- 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*cis currently this:MUL ID: a MUL ID: b ID: cWe want this:
MUL MUL ID: a ID: b ID: c- In
parser.c:- get rid of
parse_exp_prime. Modify
parse_expby 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; }
- get rid of
- 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
- Let's handle addition.
The new grammar:
Exp ::= Term {"+" Term} Term ::= Id {"*" Id}- In
parser.c:Instead of adding a
make_addfunction (which would look almost the same asmake_mul), get rid ofmake_muland 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
Exprule changed, modifyparse_expaccordingly: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_termfunction (the body is the same as the oldparse_exp's body, except that now we usemake_binaryinstead ofmake_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_ADDtoNodeType:
typedef enum { NODE_ID, NODE_MUL, NODE_ADD, } NodeType;
- add
In
scanner.h:- Add
TOKEN_PLUStoTokenType:
typedef enum { TOKEN_PLUS, TOKEN_STAR, TOKEN_ID, TOKEN_ERROR, TOKEN_EOF, } TokenType;
- Add
- in
scanner.c:Add a
case '+'in theswitchinnext_token:Token next_token(Scanner *scanner) { //... case '+': return make_token(scanner, TOKEN_PLUS); //... }
- In
printer.c:add a
case NODE_ADDinnode_type_to_strandprint_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
- Let's handle subtraction.
- In
parser.c:Modify the
Exprule 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_SUBtoNodeType: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_SUBinnode_type_to_strandprint_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
- Let's now handle division.
- In
parser.c:Update the
Termrule 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_DIVinnode_type_to_strandprint_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
- In
parser.c:Modify the
Termrule and add aFactorrule:Exp ::= Term {("+"|"-") Term} Term ::= Factor {("*"|"/") Factor} Factor ::= "(" Exp ")" | IdThe
Termrule has changed. Modifyparse_termaccordingly: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)inscanToken:Token scan_token() { // ... case '(': return make_token(TOKEN_OPEN_PAREN); case ')': return make_token(TOKEN_CLOSE_PAREN); // ... }
- In
scanner.h:Add
TOKEN_OPEN_PARENandTOKEN_CLOSE_PARENinTokenType: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
- Let's get rid of the
a,b,cvariables and use numbers instead, so that we can parse expressions like1+22*333. Replace
IdwithNumLiteralin theFactorrule: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_expandparse_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_idwith a call toparse_numinparse_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); } }
- Get rid of
- In
parser.h:Add a
NODE_NUM_LITERAL in ~NodeType~:typedef enum { NODE_NUM_LITERAL, // ... } NodeType;
Update the
dataunion inASTNode, by getting rid ofidand addingnum_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
NumLiteralin 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_tokenby 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_LITERALinTokenType: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_IDcases innode_type_to_strandprint_astwith those forNODE_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
Update the grammar byt modifying
Factorand addingPrimary: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 oldparse_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_NEGinNodeType:typedef enum { NODE_NEG, // ... } NodeType;
add a
unarymember in thedataunion inASTNode:typedef struct ASTNode { // ... union { // ... struct { struct ASTNode *node; } unary; // ... } data; } ASTNode;
- In printer.c:
Add a
NODE_NEGcase innode_type_to_strandprint_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
New
Factorrule:Factor ::= ("+"|"-") Factor | PrimaryModify
parse_factor(we don't need to createPOSnodes, 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
- Aho, Sethi, Ullman: Compilers: Principles, Techniques, and Tools. (The Dragon Book)
- Nystrom: Crafting Interpreters.
- Cooper, Torczon: Engineering a Compiler.
- Kay Lack: 0de5.
- Relevant exchange on hn: https://news.ycombinator.com/item?id=39317161
- https://gokcehan.github.io/notes/recursive-descent-parsing.html