惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

G
GRAHAM CLULEY
S
Security @ Cisco Blogs
P
Proofpoint News Feed
Cisco Talos Blog
Cisco Talos Blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
C
Cyber Attacks, Cyber Crime and Cyber Security
T
Tor Project blog
WordPress大学
WordPress大学
Project Zero
Project Zero
S
Schneier on Security
P
Proofpoint News Feed
小众软件
小众软件
P
Privacy International News Feed
美团技术团队
L
LangChain Blog
Know Your Adversary
Know Your Adversary
J
Java Code Geeks
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
The Register - Security
The Register - Security
N
Netflix TechBlog - Medium
Microsoft Security Blog
Microsoft Security Blog
Engineering at Meta
Engineering at Meta
I
InfoQ
量子位
Vercel News
Vercel News
博客园 - 三生石上(FineUI控件)
Spread Privacy
Spread Privacy
D
DataBreaches.Net
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
U
Unit 42
P
Privacy & Cybersecurity Law Blog
C
Cybersecurity and Infrastructure Security Agency CISA
T
The Blog of Author Tim Ferriss
Latest news
Latest news
K
Kaspersky official blog
MongoDB | Blog
MongoDB | Blog
L
LINUX DO - 热门话题
Simon Willison's Weblog
Simon Willison's Weblog
云风的 BLOG
云风的 BLOG
S
Securelist
AWS News Blog
AWS News Blog
F
Fortinet All Blogs
T
Threat Research - Cisco Blogs
Stack Overflow Blog
Stack Overflow Blog
Scott Helme
Scott Helme
Help Net Security
Help Net Security
Y
Y Combinator Blog
宝玉的分享
宝玉的分享
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
T
Tenable Blog

Minko Gechev's blog

skillgrade Unit Tests for AI Agent Skills You Should Care About AI Generative Development LLM-first Web Framework Reactive framework in ~200 lines of JavaScript Managing Angular Are LLMs going to replace us? Prefetching Heuristics Design Patterns in Open Source Projects - Part II Design Patterns in Open Source Projects - Part I What I learned doing 125 public talks - Part I Dynamic imports solve all the problems, right? 5 Angular CLI Features You Didn't Know About Angular quicklink Preloading Strategy Introducing Bazel Schematics for Angular CLI Building TypeScript Projects with Bazel Joining Google Playing Mortal Kombat with TensorFlow.js. Transfer learning and data augmentation Fast, extensible, configurable, and beautiful linter for Go Introducing Guess.js - a toolkit for enabling data-driven user-experiences on the Web Machine Learning-Driven Bundling. The Future of JavaScript Tooling. JavaScript Decorators for Declarative and Readable Code 3 Tricks For Using Redux and Immutable.js with TypeScript Follow Your Dream Career with Open Source. Personal Story. Redux Anti-Patterns - Part 1. State Management. Faster Angular Applications - Understanding Differs. Developing a Custom IterableDiffer Faster Angular Applications - Part 2. Pure Pipes, Pure Functions and Memoization Faster Angular Applications - Part 1. On Push Change Detection and Immutability Understanding Dynamic Scoping and TemplateRef Developing Statically Typed Programming Language WebVR for a Gamified IDE 7 Angular Tools That You Should Consider Announcing ngrev - Reverse Engineering Tool for Angular Implementing Angular's Dependency Injection in React. Understanding Element Injectors. Distributing an Angular Library - The Brief Guide Angular in Production Ahead-of-Time Compilation in Angular 2.5X Smaller Angular 2 Applications with Google Closure Compiler Using Stripe with Angular (Deprecated) Building an Angular Application for Production Implementing the Missing "resolve" Feature of the Angular 2 Router Scalable Single-Page Application Architecture Managing ambient type definitions and dealing with the "Duplicate identifier" TypeScript error Static Code Analysis of Angular 2 and TypeScript Projects Enforcing Best Practices with Static Code Analysis of Angular 2 Projects ViewChildren and ContentChildren in Angular Dynamically Configuring the Angular's Router Angular 2 Hot Loader Lazy Loading of Route Components in Angular 2 Aspect-Oriented Programming in JavaScript Flux in Depth. Store and Network Communication. Using JSX with TypeScript Flux in Depth. Overview and Components. Even Faster AngularJS Data Structures Boost the Performance of an AngularJS Application Using Immutable Data - Part 2 Angular2 - First Impressions Build Your own Simplified AngularJS in 200 Lines of JavaScript Persistent State of ReactJS Component Boost the Performance of an AngularJS Application Using Immutable Data Processing Binary Protocols with Client-Side JavaScript Stream your Desktop to HTML5 Video Element Multi-User Video Conference with WebRTC Asynchronous calls with ES6 generators Binary Tree iterator with ES6 generators WebRTC chat with React.js AngularJS in Patterns (Part 3) AngularJS in Patterns (Part 2). Services. Using GitHub Pages with Jekyll! AngularJS in Patterns (Part 1). Overview of AngularJS Singleton in JavaScript Express over HTTPS What I get from the JavaScript MV* frameworks Remote Desktop Client with AngularJS and Yeoman The magic of $resource (or simply a client-side Active Record) AngularJS Inheritance Patterns AngularAOP v0.1.0 Advanced JavaScript at Sofia University AngularJS style guide Lazy prefetching of AngularJS partials VNC client on 200 lines of JavaScript Aspect-Oriented Programming with AngularJS CSS3 flipping effect Practical programming with JavaScript Why I should use publish/subscribe in JavaScript JavaScript, the weird parts Functional programming with JavaScript plainvm Looking for performance? Probably you should NOT use [].sort (V8) JavaScript image scaling ELang Caching CSS with localStorage Self-invoking functions in JavaScript (or Immediately Invoked Function Expressions) Asus N56VZ + Ubuntu 12.04 (en) Asus N56VZ + Ubuntu 12.04 Debian Squeeze + LXDE on Google Nexus S (or having some fun while suffering) HTML5 image editor Курсови проекти – ФМИ Carousel Gallery SofiaJS...
Implementing a Simple Compiler on 25 Lines of JavaScript
2017-09-16 · via Minko Gechev's blog

Edit · Sep 16, 2017 · 12 minutes read · Follow @mgechev Compilers Lexical Analysis Syntax Analysis Computer Science

I already wrote a couple of essays related to the development of programming languages that I was extremely excited about! For instance, in “Static Code Analysis of Angular 2 and TypeScript Projects"[1] I explored the basics of the front end of the compilers, explaining the phases of lexical analysis, syntax analysis and abstract-syntax trees.

Recently I published “Developing Statically Typed Programming Language"[2]. This post shown a simple, statically typed, functional programming language inspired by lambda calculus. There I outsourced the front end part of the compiler development to a parser generator and focused on the back end in the faces of a module for type checking and one for code generation.

Why do I need this?

You might be now wondering “Why would I need to know how to develop a compiler?”. There are a few important reasons:

  • You will get a better understanding of how the programming languages you’re using work. This will allow you to develop more efficient programs with them.
  • Often, you’ll have to reuse pieces of the modules described below for different purposes (for instance, parsing of configuration files, parsing of network messages, etc.).
  • Creating a DSL. Creating a Domain Specific Language in your project could be quite handy in order to simplify tasks which otherwise take a lot of time to solve with a general purpose programming language.

What are we going to cover?

In this blog post we’ll cover the basics from end-to-end! We’ll develop an extremely simple compiler on 25 lines of JavaScript! Our compiler will have:

  • Module for lexical analysis
  • Module for syntax analysis
    • The parser will be based on an EBNF grammar
    • We will develop the parser by using a recursive descent parsing algorithm
  • Code generator

The language that we’re going to explore is not particularly useful for developing meaningful software programs but it can be easily extended to one.

The entire implementation can be found at my GitHub profile[3].

Simplicity

Introducing a Simple Prefix Language

Here’s how a sample expression in our language is going to look like:

By the end of this article we’ll be able to transpile these expressions to JavaScript by going through phases typical for any compiler!

For simplicity, there are a few rules we need to follow:

  • We have only the functions: mul, sub, sum, div.
  • Each individual string token is surrounded by whitespace.
  • We support only natural numbers.

In order to explore the semantics behind the expression above let’s define a few JavaScript functions:

const mul = (...operands) => operands.reduce((a, c) => a * c, 1);
const sub = (...operands) => operands.reduce((a, c) => a - c);
const sum = (...operands) => operands.reduce((a, c) => a + c, 0);

mul accepts multiple operands, passed with the spread operator. The function just multiplies all of them, so for instance mul(2, 3, 4) === 24. sub respectively subtracts the passed arguments and sum sums them.

The expression above can be translated to the following JavaScript expression:

mul(3, sub(2, sum(1, 3, 4)))

or…

Now, after we have understanding of the semantics, let’s start with the front end of the compiler!

Note: Similar prefix expressions can be simply evaluated with a stack-based algorithm, however, in this case we’ll focus on concepts rather than implementation.

Developing the Compiler’s Front End

The front end of any compiler usually has the modules for Lexical Analysis[4] and Syntax Analysis[5]. In this section we’ll build both modules in a few lines of JavaScript!

Developing a Lexical Analyzer

The phase of lexical analysis is responsible for dividing the input string (or stream of characters) of the program into smaller pieces called tokens. The tokens usually carry information about their type (if they are numbers, operators, keywords, identifiers, etc), the substring of the program they represent and their position in the program. The position is usually used for reporting user friendly errors in case of invalid syntactical constructs.

For instance, if we have the JavaScript program:

A sample lexical analyzer for JavaScript will produce the output:

[
  {
    lexeme: 'if',
    type: 'keyword',
    position: {
      row: 0,
      col: 0
    }
  },
  {
    lexeme: '(',
    type: 'open_paran',
    position: {
      row: 0,
      col: 3
    }
  },
  {
    lexeme: 'foo',
    type: 'identifier',
    position: {
      row: 0,
      col: 4
    }
  },
  ...
]

We’ll keep our lexical analyzer as simple as possible. In fact, we’ll implement it on a single line of JavaScript:

const lex = str => str.split(' ').map(s => s.trim()).filter(s => s.length);

Here we split the string by a single space, we map the produced substrings to their trimmed version and filter the empty strings.

Invoking the lexer with an expression will produce an array of strings:

lex('mul 3 sub 2 sum 1 3 4')

// ["mul", "3", "sub", "2", "sum", "1", "3", "4"]

This is completely enough for our purpose!

Now let’s go to the phase of syntax analysis!

Developing a Parser

The syntax analyzer (often known as parser) is the module of a compiler which out of a list (or stream) of tokens produces an Abstract Syntax Tree[6] (or in short an AST). Along the process, the syntax analyzer may also produce syntax errors in case of invalid programs.

Nature Tree

Usually, the parser is implemented based on a grammar[7]. Here’s the grammar of our language:

digit = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
num = digit+
op = sum | sub | mul | div
expr = num | op expr+

This basically means that we have digits which composed together can form numbers (num). We have 4 operations and an expression which can be either a number, or operation, followed by one or more expressions. We’ll refer to the individual definitions in the grammar (for instance to num and op) as rules.

This is the so called EBNF grammar[6]. Look at the grammar for a bit, try to understand it, and after that completely forget about it! We’ll come back to the grammar after we explain the parser and you’ll see how everything connects together!

As we mentioned, a parser is a tool which turns a list of tokens to an AST.

For instance, for our expression:

The parser will produce the following AST, based on the grammar above:

Let’s explore the algorithm for this!

const Op = Symbol('op');
const Num = Symbol('num');

const parse = tokens => {

  let c = 0;

  const peek = () => tokens[c];
  const consume = () => tokens[c++];

  const parseNum = () => ({ val: parseInt(consume()), type: Num });

  const parseOp = () => {
    const node = { val: consume(), type: Op, expr: [] };
    while (peek()) node.expr.push(parseExpr());
    return node;
  };

  const parseExpr = () => /\d/.test(peek()) ? parseNum() : parseOp();

  return parseExpr();
};

The bad news is that there are a lot of things going on. The good news is that this is the most complicated part of the compiler!

Let’s divide the code into parts and look into each one step by step.

Node Types

const Op = Symbol('op');
const Num = Symbol('num');

First we define the different node types that we are going to have in the AST. We’ll have only numbers and operations. For example, the number node 42 will look like this:

The operator sum, applied to 2, 3, 4 will look like this:

{
  type: Op,
  val: 'sum',
  expr: [{
    type: Num,
    val: 2
  }, {
    type: Num,
    val: 3
  }, {
    type: Num,
    val: 4
  }]
}

That’s how simple is that!

The Parser

After we declare the node types, we define a function called parse which accepts a single argument called tokens. Inside of it we define five more functions:

  • peek - returns the element of tokens associated with the current value of the c local variable.
  • consume - returns the element of tokens associated with the current value of the c local variable and increments c.
  • parseNum - gets the current token (i.e. invokes peek()), parses it to a natural number and returns a new number token.
  • parseOp - we’ll explore in a little bit.
  • parseExpr - checks if the current token matches the regular expression /\d/ (i.e. is a number) and invokes parseNum if the match was successful, otherwise returns parseOp.

Parsing Operations

The parseOp is maybe the most complicated function from the parser above. That’s the case because of the loop and the indirect recursion that we have. Here’s its definition one more time in order to explain it line by line:

const parseOp = () => {
  const node = { val: consume(), type: Op, expr: [] };
  while (peek()) node.expr.push(parseExpr());
  return node;
};

Since parseOp has been invoked by parseExpr when the value of peek() is not a number we know that it is an operator so we create a new operation node. Note that we don’t perform any further validation, however, in a real-world programming language we’d want to do that and eventually throw a syntax error in case of unexpected token.

Anyhow, in the node declaration we set the list of “sub-expressions” to be the empty list (i.e. []), the operation name to the value of consume() and the type of the node to Op. Later, while we don’t reach the end of the program, we loop over all tokens by pushing the currently parsed expression to the list of “sub-expressions` of the given node. Finally, we return the node.

Keep in mind that while (peek()) node.expr.push(parseExpr()); performs an indirect recursion. In case we have the expression:

This will

  • First, invoke parseExpr, which will find that the current token (i.e. tokens[0]) is not a number (it’s sum) so it’ll invoke parseOp.
  • After that parseOp will create the operation node and because of the consume() call, increment the value of c.
  • Next parseOp will iterate over the tokens, and for tokens[c], where c now equals 1 will invoke parseExpr.
  • parseExpr will find that the current token is not a number so it’ll invoke parseOp.
  • parseOp will create another operation node and increment c and will start looping over the remaining tokens again.
  • parseOp will invoke parseExpr where c will now equal 2.
  • Since tokens[2] === "2", parseExpr will invoke parseNum which will create a number node, incrementing the c variable.
  • parseNum will return the number node and it will be pushed into the expr array of the last operation node produced by the latest parseOp invocation.
  • The last parseOp invocation will return the operation node since peek() will return undefined (parseNum has incremented c to 3 and tokens[3] === undefined).
  • The node returned by the last invocation of parseOp will be returned to the outermost invocation of parseOp which will return its operation node as well.
  • Finally, parseExpr will return the root operation node.

The produced AST will look like:

{
  type: Op,
  val: "sum",
  expr: [{
    type: Op,
    val: "sum",
    expr: [{
      type: Num,
      val: 2
    }]
  }]
}

…and that’s it! The final step is to traverse this tree and produce JavaScript!

Recursive Descent Parsing

Now let’s see how are the individual functions related to the grammar we defined above and see why having a grammar makes sense in general. Let’s take a look at the rules in the EBNF grammar:

digit = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
num = digit+
op = sum | sub | mul | div
expr = num | op expr+

Do they now may make a bit more sense? expr looks very much like parseExpr, where we parse either a number or an operation. Similarly, op expr+ looks very much like parseOp and num like parseNum. In fact, very often parsers are generated directly from the grammars since there’s a direct connection between both with the recursive descent parsing algorithm[8].

And in fact, we just developed a simple recursive descent parser! Our parser was quite simple (well, we have only 4 production rules in the grammar) but you can imagine how complex the parser of a real-life programming language is.

It’s extremely convenient to develop the grammar of a language before writing the actual parser in order to observe a simplified model of it. The parser contains a lot of details (for instance a lot of syntax constructs of the language you’re developing it with), in contrast to the grammar which is extremely simplified and minimalistic.

Developing the Transpiler

In this part of the post we’ll traverse the AST of the language and produce JavaScript. The entire transpiler is on 7 lines of JavaScript (literally!):

const transpile = ast => {
  const opMap = { sum: '+', mul: '*', sub: '-', div: '/' };
  const transpileNode = ast => ast.type === Num ? transpileNum(ast) : transpileOp(ast);
  const transpileNum = ast => ast.val;
  const transpileOp = ast => `(${ast.expr.map(transpileNode).join(' ' + opMap[ast.val] + ' ')})`;
  return transpileNode(ast);
};

Let’s explore the implementation line by line.

First we define a function called transpile. It accepts as argument the AST produced by the parser. After that in the opMap we define the mapping between arithmetic operations and the operators in the language. Basically, sum maps to +, mul to *, etc.

As next step, we define the function transpileNode which accepts an AST node. If the node is a number, we invoke the transpileNum function with the given node, otherwise, we invoke transpileOp.

Finally, we define the two functions for transpilation of the individual nodes:

  • transpileNum - translates a number to a JavaScript number (simply by returning it).
  • transpileOp - translates an operation to a JavaScript arithmetic operation. Notice that we have indirect recursion here (transpileOp -> transpileNode -> transpileOp). For each operation node, we want to transpile its sub-expressions first. We do that by invoking the transpileNode function.

On the last line of transpile’s body, we return the result of transpileNode applied to the root of the tree.

Wiring Everything Together

Here’s how we can wire everything together:

const program = 'mul 3 sub 2 sum 1 3 4';

transpile(parse(lex(program)));
// (3 * (2 - (1 + 3 + 4)))

We invoke lex(program), which produces the list of tokens, after that we pass the tokens to the parse function, which produces the AST and finally, we transpile the AST to JavaScript!

Connecting

Conclusion

This article explained in details the development of a very simple compiler (or transpiler) of a language with prefix expressions to JavaScript. Although this was explanation of only the very basics of the compiler development we were able to cover few very important concepts:

  • Lexical analysis
  • Syntax analysis
  • Source code generation
  • EBNF grammars
  • Recursive Descent Parsing

If you’re interested in further reading, I’d recommend:

References

  1. Static Code Analysis of Angular 2 and TypeScript Projects - https://blog.mgechev.com/2016/02/29/static-code-analysis-angular-typescript/.
  2. Developing Statically Typed Programming Language - https://blog.mgechev.com/2017/08/05/typed-lambda-calculus-create-type-checker-transpiler-compiler-javascript/
  3. Tiny Compiler - https://github.com/mgechev/tiny-compiler
  4. Lexical Analysis - https://en.wikibooks.org/wiki/Compiler_Construction/Lexical_analysis
  5. Syntax Analysis - https://en.wikibooks.org/wiki/Compiler_Construction/Syntax_Analysis
  6. Abstract Syntax Tree - https://en.wikibooks.org/wiki/Compiler_Construction/Case_Study_1B#Abstract_Syntax_Trees
  7. EBNF grammar - https://en.wikipedia.org/wiki/Extended_Backus%E2%80%93Naur_form
  8. Recursive Descent Parsing - https://en.wikipedia.org/wiki/Recursive_descent_parser