TT Lab
Get started
Learn Learning paths Courses

Compilers — Build a Small Language from Start to Finish

Stop gcc Midway and Build a Calculator Twice

Continue in TT Lab

Goal

You stop gcc at each stage of preprocessing, compiling, assembling, and linking to check the outputs, build the same RPN computation in two ways — an interpreter that computes directly and a compiler that translates to C and bakes it — and compare whether the two give the same answers.

Why it matters

A compiler and an interpreter use the same front end and differ only in the last step. Because of that one step, there arise the point at which errors are caught (before running or during), the speed (over how many runs the translation cost is split), and the risk of the meanings drifting apart. If an interpreter written in Python uses Python's division rules as they are, its answers diverge on negative numbers from the side baked in C. This lab makes you experience that difference by hand in a small calculator.

The language of the RPN calculator

낱말      정수(부호가 붙을 수 있음, 예: 7, -3) · x(실행할 때 받는 값) · + - * / %
뜻        낱말을 왼쪽부터 읽는다. 수는 스택에 올리고, 연산자는 두 값을 내려(먼저 내린 것이 오른쪽)
          계산한 결과를 올린다. 끝에 값이 정확히 하나 남아야 한다.
나눗셈    C 처럼 0 쪽으로 자른다: -7 / 2 = -3, 나머지의 부호는 나눠지는 수를 따른다: -7 % 2 = -1
예        "x 1 + 2 *" 는 (x + 1) * 2
오류 글자 bad token 'foo' at 3      (1부터 센 낱말 자리)
          stack underflow at 2      (그 연산자의 자리)
          expected one value, got 2 (끝에 남은 값의 수)
          division by zero

Steps

  1. Stop /opt/fixtures/mini/c/hello.c at each stage to produce /root/mini/stages/hello.i (preprocessed), hello.s (assembly), hello.o (object file), and hello (executable). Check the names that are not yet resolved (U) with nm hello.o.
  2. Fill in tokenize(text) in /root/mini/rpn.py — the list of words split on whitespace, and for an unknown word, RpnError("bad token 'foo' at 3").
  3. Fill in evaluate(tokens, x) — an interpreter that computes directly with a stack. Errors are RpnError with exactly the text above.
  4. Fill in check(tokens) — it does not run but only follows the stack depth, and returns the number of slots at the deepest point if there is no problem, or an RpnError with the same text as evaluate if there is. Division by zero depends on x, so you do not judge it here.
  5. Fill in to_c(tokens) — the text of a C program that does the same computation. First check with check, and if the program is wrong, do not write C and raise RpnError. x comes from argv[1], the result is printed on one line, and on division by zero it prints runtime error: division by zero and exits with 1.
  6. Fill in build(tokens, out) — make out.c → out.s → out.o → out in turn with gcc and do not delete the intermediate outputs.
  7. Compare your interpreter and your compiler against each other. The grader runs 15 random programs through both paths with four values of x — check that they do not diverge on negative division and remainder.
  8. Fill in bench(tokens, xs, out), and write the result of measuring /opt/fixtures/mini/rpn/big.rpn for x = 0 to 199 into /root/mini/rpn_report.json as it is.

Notes

Stop gcc four times

gcc can stop at -E (preprocess), -S (compile), and -c (assemble), and with no option it goes all the way to linking. If you give the output of the earlier stage as the input of the next stage, you can look at it one stage at a time. The .o does not yet have the address of printf, so nm marks it with U.

Split into words

After splitting with text.split(), check for each word whether it is x, an operator, or an integer. An integer is a run of digits that may have one sign attached ('--3' is not an integer). Positions are counted from 1.

Compute directly with a stack — the interpreter

When you meet an operator, pop two values. The one popped first is the right operand. For division, get the quotient with abs and then attach the sign to truncate toward 0, and get the remainder as a - quotient*b, and it becomes the same as C.

Check without running

Count only the depth of the stack, not the values. A number is +1, and an operator pops two and pushes one, so it is -1, and before that you check that the depth is at least 2. The deepest value becomes the size of the C array.

Translate the same computation into C — the compiler

If you also keep one stack array and a slot index (sp) in C, each word translates as it is, one line at a time. The array size is the depth that check measured. C's / and % already truncate toward 0, so there is nothing more to do, but dividing by 0 kills the process, so check before that.

C → assembly → object file → executable

You call, with subprocess in turn, the four stages you did by hand in step 1 (gcc -S, gcc -c, gcc). If any one stage fails, put the last line of the gcc error in an RpnError and raise it.

Compare whether the two paths give the same answers

You write no new code in this step. The grader runs random programs through both your evaluate and your build result. If it fails, check the sign of negative division and remainder first — if the interpreter follows Python's rules, it diverges from C.

Measure when compiling pays off

You measure three intervals with time.perf_counter — evaluate per value, build once, and launching the executable per value. The numbers also show that the cost of launching a process can be larger than the computation. same is whether the two result lists are equal.