Is there any difference between the two? As per Ullman's book, compilers convert one language to another (usually low level) language, and so does an assembler. How are the two different?
An assembler translates assembly code to machine code. The translation is mechanical, and can be done in only one way. In contrast, a compiler has more freedom when it compiles the relevant programming language - it can optimize, for example, and even non-optimizing compilers produce different code. Also, compilers can be written in a way that separates the "front-end" (corresponding to the programming language) and the "back-end" (corresponding to the computer architecture), whereas with assemblers the two are always the same.
The bottom line is that it is more fun to write a compiler than an assembler. Assembly languages are usually designed to be nearly trivial to parse and type check and tend to involve a lot of table-driven generators ("the opcode for add is 01110", "for load instructions the destination operand register is specified by bits 17 through 21"). Usually the most interesting part of an assembler is the part that resolves symbolic labels into numbers.
However, most assemblers can do a small amount of arithmetic (adding together symbolic labels with small constants, for example) and most assemblers either have or are integrated with a macro processing facility. (On most Unix systems the macro feature is actually provided by running the C pre-processor over the assembly before passing it to the assembler proper.)
The MIPS assembler had to go a step beyond that and made some interesting code generation decisions and did a small amount of optimization. The MIPS machine language requires different code sequences to load different constants, for example, and so the assembler had to choose the code sequence after constructing the constant. Further, MIPS machine code had the notion of delay slots, but it was the responsibility of the assembler to abstract these away and present a more "normal" abstract assembly language to the compiler. So the MIPS assembler needs to do some local instruction scheduling.
The distinction is further blurred by some of Norman Ramsey's work, in particular his C-- portable assembly language. (The relevant paper is Ramsey and Peyton Jones, "A Single Intermediate Language That Supports Multiple Implementations of Exceptions", Prog. Lang. Impl. and Dsgn., (PLDI-21):285–298, 2000.) And finally, there is also a Typed Assembly Language from David Walker and Greg Morrisett with an assembler that can guarantee memory safety.
A bit of simplified answer here, the reality is more complicated. I would expect the difference between an Assembler (A) and a Compiler (C) to be among other things:
- One line of source code relates directly to one CPU opcode (A) or not (C)
- Highly dependent on the actual CPU (A) or machine independant (C)
We tend to call assembly language "low level" and the source language a compiler understands "high level" (this is gross simplification, but still).
In assembly language you could as example do an add operation by saying:
- add a,b (for one specific CPU)
- add R5,R6 (for a different CPU)
- add (A5),D2 (for a different CPU)
In a high level language you might write:
- x = y + z ;
And this could result in one instruction or hundreds of instructions depending on a number of circumstances, one is what CPU the compiler creates instructions for.
As you can see the assembly source language is most often: (A) one line of source code gives one line of CPU opcodes and it very much depends on which CPU you are targeting. A high level language (C) compiler handles all these details for you -- one line of source code could become zero, one or many CPU opcodes and the compiler handles the details of what the CPU can do.
A compiler today often consists of several different stages. They could be named frontend / backend or beeing called other things. I typically see them as four stages:
- The first stage reads the actual source code and creates an internal representation. This stage knows the actual source language.
- The second stage looks at the internal representation and does a number of optimizations. Nowadays, the compiler would typically look for making the program faster and not caring for if it gets larger. The optimizing is done on the internal representation. Interestingly, parts of this could be generic for several different languages.
- The third stage takes the internal representation and creates actual code for the selected CPU. There might be several different versions of this stage, targeting different CPU-s. In effect you could write source code once and then compile it for different CPUS-s.
- Final preparations for "packaging" the program (this stage could be a linker).
Writing good compilers is a highly skilled profession -- making a toy language compiler can be done in an afternoon by an amatuer (or well, slightly longer).