290

Advanced compilers like gcc compile code into machine readable files according to the language in which the code has been written (e.g. C, C++, etc). In fact, they interpret the meaning of the code according to library and functions of the corresponding languages. Correct me if I'm wrong.

I wish to better understand compilers by writing a very basic compiler (probably in C) to compile a static file (e.g. Hello World in a text file). I tried some tutorials and books, but all of them are for practical cases. They deal with compiling dynamic code with meanings connected with the corresponding language.

How can I write a basic compiler to convert a static text into a machine readable file?

The next step will be introducing variables into the compiler; imagine that we want to write a compiler which compile only some functions of a language.

Introducing practical tutorials and resources is highly appreciated :-)

5

7 Answers 7

436

Intro

A typical compiler does the following steps:

  • Parsing: the source text is converted to an abstract syntax tree (AST).
  • Resolution of references to other modules (C postpones this step till linking).
  • Semantic validation: weeding out syntactically correct statements that make no sense, e.g. unreachable code or duplicate declarations.
  • Equivalent transformations and high-level optimization: the AST is transformed to represent a more efficient computation with the same semantics. This includes e.g. early calculation of common subexpressions and constant expressions, eliminating excessive local assignments (see also SSA), etc.
  • Code generation: the AST is transformed into linear low-level code, with jumps, register allocation and the like. Some function calls can be inlined at this stage, some loops unrolled, etc.
  • Peephole optimization: the low-level code is scanned for simple local inefficiencies which are eliminated.

Most modern compilers (for instance, gcc and clang) repeat the last two steps once more. They use an intermediate low-level but platform-independent language for initial code generation. Then that language is converted into platform-specific code (x86, ARM, etc) doing roughly the same thing in a platform-optimized way. This includes e.g. the use of vector instructions when possible, instruction reordering to increase branch prediction efficiency, and so on.

After that, object code is ready for linking. Most native-code compilers know how to call a linker to produce an executable, but it's not a compilation step per se. In languages like Java and C# linking may be totally dynamic, done by the VM at load time.

Remember the basics

  • Make it work
  • Make it beautiful
  • Make it efficient

This classic sequence applies to all software development, but bears repetition.

Concentrate on the first step of the sequence. Create the simplest thing that could possibly work.

Read the books!

Read the Dragon Book by Aho and Ullman. This is classic and is still quite applicable today.

Modern Compiler Design is also praised.

If this stuff is too hard for you right now, read some intros on parsing first; usually parsing libraries include intros and examples.

Make sure you're comfortable working with graphs, especially trees. These things is the stuff programs are made of on the logical level.

Define your language well

Use whatever notation you want, but make sure you have a complete and consistent description of your language. This includes both syntax and semantics.

It's high time to write snippets of code in your new language as test cases for the future compiler.

Use your favorite language

It's totally OK to write a compiler in Python or Ruby or whatever language is easy for you. Use simple algorithms you understand well. The first version does not have to be fast, or efficient, or feature-complete. It only needs to be correct enough and easy to modify.

It's also OK to write different stages of a compiler in different languages, if needed.

Prepare to write a lot of tests

Your entire language should be covered by test cases; effectively it will be defined by them. Get well-acquainted with your preferred testing framework. Write tests from day one. Concentrate on 'positive' tests that accept correct code, as opposed to detection of incorrect code.

Run all the tests regularly. Fix broken tests before proceeding. It would be a shame to end up with an ill-defined language that cannot accept valid code.

Create a good parser

Parser generators are many. Pick whatever you want. You may also write your own parser from scratch, but it only worth it if syntax of your language is dead simple.

The parser should detect and report syntax errors. Write a lot of test cases, both positive and negative; reuse the code you wrote while defining the language.

Output of your parser is an abstract syntax tree.

If your language has modules, the output of the parser may be the simplest representation of 'object code' you generate. There are plenty of simple ways to dump a tree to a file and to quickly load it back.

Create a semantic validator

Most probably your language allows for syntactically correct constructions that may make no sense in certain contexts. An example is a duplicate declaration of the same variable or passing a parameter of a wrong type. The validator will detect such errors looking at the tree.

The validator will also resolve references to other modules written in your language, load these other modules and use in the validation process. For instance, this step will make sure that the number of parameters passed to a function from another module is correct.

Again, write and run a lot of test cases. Trivial cases are as indispensable at troubleshooting as smart and complex.

Generate code

Use the simplest techniques you know. Often it's OK to directly translate a language construct (like an if statement) to a lightly-parametrized code template, not unlike an HTML template.

Again, ignore efficiency and concentrate on correctness.

Target a platform-independent low-level VM

I suppose that you ignore low-level stuff unless you're keenly interested in hardware-specific details. These details are gory and complex.

Your options:

  • LLVM: allows for efficient machine code generation, usually for x86 and ARM.
  • CLR: targets .NET, multiplatform; has a good JIT.
  • JVM: targets Java world, quite multiplatform, has a good JIT.

Ignore optimization

Optimization is hard. Almost always optimization is premature. Generate inefficient but correct code. Implement the whole language before you try to optimize the resulting code.

Of course, trivial optimizations are OK to introduce. But avoid any cunning, hairy stuff before your compiler is stable.

So what?

If all this stuff is not too intimidating for you, please proceed! For a simple language, each of the steps may be simpler than you might think.

Seeing a 'Hello world' from a program that your compiler created might be worth the effort.

13
  • 62
    This is one of the best answers I've seen yet.
    – gahooa
    Commented Sep 20, 2012 at 21:12
  • 13
    I think you missed a part of the question... The OP wanted to write a very basic compiler. I think you go beyond very basic here. Commented Sep 21, 2012 at 12:01
  • 28
    @marco-fiset, on the contrary, I think it's an outstanding answer that does tell the OP how to do a very basic compiler, while pointing out the traps to avoid and defining more advanced phases.
    – smci
    Commented Mar 3, 2013 at 22:50
  • 6
    This is one of the best answers I have ever seen in the entire Stack Exchange universe. Kudos!
    – airstrike
    Commented Dec 8, 2015 at 21:12
  • 6
    Seeing a 'Hello world' from a program that your compiler created might be worth the effort. -- INDEED
    – slier
    Commented Aug 11, 2016 at 2:56
39

Jack Crenshaw's Let's Build a Compiler, while unfinished, is an eminently readable introduction and tutorial.

Nicklaus Wirth's Compiler Construction is a very good textbook on the basics of simple compiler construction. He focuses on top-down recursive descent, which, let's face it, is a LOT easier than lex/yacc or flex/bison. The original PASCAL compiler that his group wrote was done this way.

Other people have mentioned the various Dragon books.

4
  • 1
    One of the nice things about Pascal, is that everything has to be defined or declared before being used. Therefore it can be compiled in one pass. Turbo Pascal 3.0 is one such example, and there is a lot of documentation about the internals here.
    – tcrosley
    Commented Oct 20, 2015 at 2:19
  • 1
    PASCAL was specifically designed with one-pass compilation and linking in mind. Wirth's compiler book mentions multipass compilers, and adds that he knew of a PL/I compiler that took 70 (yes, seventy) passes. Commented Oct 20, 2015 at 16:24
  • Mandatory declaration before use dates back to ALGOL. Tony Hoare got his ears pinned back by the ALGOL committee when he tried to suggest adding default type rules, similar to what FORTRAN had. They already knew about the problems this could create, with typographical errors in names and default rules creating interesting bugs. Commented Oct 20, 2015 at 16:25
  • 2
    Here is a more updated and finished version of the book by the original author himself: stack.nl/~marcov/compiler.pdf Please edit your answer and add this :)
    – user169477
    Commented Oct 27, 2016 at 15:33
16

If you really want to write machine readable code only and not targeted to a virtual machine, then you will have to read Intel manuals and understand

  • a. Linking and Loading Executable code

  • b. COFF and PE formats(for windows), alternatively understand ELF format(for Linux)

  • c. Understand .COM file formats(easier than PE)
  • d. Understand assemblers
  • e. Understand compilers and code generation engine in compilers.

Much harder done than said. I suggest you to read Compilers and Interpreters in C++ as a starting point(By Ronald Mak). Alternatively, "lets build a compiler" by Crenshaw is OK.

If you don't want to do that, you could as well write your own VM and write a code generator targeted to that VM.

Tips: Learn Flex and Bison FIRST. Then go on to build your own compiler / VM.

Good Luck!

7
  • 10
    I think targeting LLVM and not real machine code is about the best way available today.
    – 9000
    Commented Sep 20, 2012 at 16:38
  • I agree, I have been following LLVM for sometime now and I should say it was one of the best things I had seen in years in terms of programmer effort needed to target it! Commented Sep 20, 2012 at 16:40
  • 2
    What about MIPS and use spim to run it? Or MIX?
    – user40980
    Commented Sep 20, 2012 at 16:57
  • @MichaelT I have not used MIPS but I am sure it will be good. Commented Sep 20, 2012 at 17:04
  • @PrototypeStark RISC instruction set, real world processor that is still in use today (understanding it will be translatable into embedded systems). The full instruction set is at wikipedia. Looking on the net, there are lots of examples and it is used in many academic classes as a target for machine language programming. There is a bit of activity on it at SO.
    – user40980
    Commented Sep 20, 2012 at 17:15
14

I'd actually start off with writing a compiler for Brainfuck. It's a fairly obtuse language to program in but it only has 8 instructions to implement. It's about as simple as you can possibly get and there are equivalent C instructions out there for the commands involved if you find the syntax off-putting.

2
  • 14
    But then, once you have your BF compiler ready, you have to write your code in it :( Commented Jan 28, 2016 at 18:55
  • @500-InternalServerError use the C subset method
    – user28988
    Commented Jan 28, 2016 at 19:11
12

DIY approach for simple compiler could look like this (at least that's how my uni project looked like):

  1. Define Grammar of the language. Context-free.
  2. If your grammar isn't LL(1) yet, do it now. Note, that some rules that looked ok in plain CF grammar may turn out ugly. Perhaps your language is too complex...
  3. Write Lexer which cuts stream of text into tokens (words, numbers, literals).
  4. Write top-down recursive descent parser for your grammar, which accepts or rejects input.
  5. Add syntax tree generation into your parser.
  6. Write machine code generator from the syntax tree.
  7. Profit & Beer, alternatively you can start thinking how to do smarter parser or generate better code.

There should be plenty of literature describing each step in detail.

2
  • The 7th point is what OP is asking about. Commented Sep 20, 2012 at 18:50
  • 12
    1-5 are irrelevant and do not deserve such a close attention. 6 is the most interesting part. Unfortunately, most of the books follow the same pattern, after the infamous dragon book, paying too much attention to parsing and leaving code transforms out of scope.
    – SK-logic
    Commented Sep 20, 2012 at 19:51
6

Sorry for a shameless plug!

Here are some things I've learned from my experience writing a simple toy compiler. Although it's in F# not in C, hopefully there's still something useful to you.

Theory

There’s no way around studying a bit of theory before getting down to coding.

Don’t start with classics textbooks like the Dragon Book, or “Modern Compiler Implementation”. These books are great, and you can always come back to them later, when you’re better prepared to take the most out of them. That said, it’s way more productive to first read ones geared toward beginners.

Here are two books that I’d opt for, both are freely available online.

  • Crafting Interpreters. Yes, you read that right, interpreters. Bear with me for a second. Compilers and interpreters have a lot in common and the book is exceptionally beginner-friendly. Basically, this is the perfect one to get started with.

  • Introduction to Compilers and Language Design again is really accessible, doesn’t assume any preexisting compilers knowledge, and teaches all the basics necessary to build a compiler using a hands-down approach. Also, the book's examples are in C.

Don’t try to come up with your own language just yet

Go with an existing educational language instead, and focus on learning about compilers. The following two are specifically designed for classroom use in compiler courses and by extension are great for a hobby compiler project.

ChocoPy

ChocoPy is a concise Python subset, which can be implemented in a time-frame reasonable for a hobby compiler.

Although concise, the language is quite expressive — the features include lists, classes, dynamic dispatch, nested functions. These represent exciting compiler design and implementation challenges.

ChocoPy is statically typed, which again leads to interesting implications for design and implementation.

The language being a Python subset means programs in it can be executed directly in a Python (3.6+) interpreter or an online playground. The code syntax highlighting, editing, and formatting is supported out of the box by any Python tool.

Cool 2020

Cool 2020 is a small Scala subset including classes, inheritance, virtual dispatch, and even a very simple form of pattern matching.

All of the advantages listed for ChocoPy apply to Cool 2020 too. The language is narrow in scope so that implementing a compiler takes a limited, realistic amount of time. While it’s still expressive and presents interesting design and implementation challenges.

Tools for Scala code syntax highlighting, formatting, and editing work out of the box. A Cool 2020 program can be (with just a little bit of help) directly compiled by a Scala compiler or executed in an online playground.

You might also find exploring my compiler's source code useful. This page tries to give a bit of background and describe some finer details of the implementation.

Emitting assembly

My compiler translates source code down to x86-64 assembly. Then GNU as and ld come into play to build a native executable from the assembly.

Many educational compilers emit MIPS assembly, and although it's possible to run it using an emulator, to me, running a native executable produced by your own compiler feels much more rewarding.

I also prefer emitting assembly instead of, for instance, C. It helps you understand the inner workings of various things that we, developers, often take for granted:

  • Managing a function's stack frame
  • Calling conventions
  • Finding the memory address of an identifier
  • Converting expressions into assembly
  • And so on, and so forth
0

Something that would actually be useful is a program that takes a problem description from a very limited problem set and turns it into c or C++ code that you then can copy and use.

I’d be very interested in a compiler that takes a less horrible language and translates it into shell code (just look at if statements in .sh file… absolutely hoorendous. Or when you can or must or must not put spaces around = or ==). I’ll write one when I’m retired.

Not the answer you're looking for? Browse other questions tagged or ask your own question.