IUCompilerCourse / Essentials-of-Compilation

A book about compiling Racket and Python to x86-64 assembly
1.27k stars 137 forks source link

Chapter 1 Interpreter errors #164

Closed AvihooI closed 11 months ago

AvihooI commented 11 months ago

In Chapter 1, the code in Python that implements the interpreter is the following:

..snip..

def interp_stmt(s):
    match s:
        case Expr(Call(Name('print'), [arg])):
            print(interp_exp(arg))
        case Expr(value):
            interp_exp(value)

def interp_Lint(p):
    match p:
        case Module(body):
            for s in body:
                interp_stmt(s)

Both interp_stmt(s) and interp_Lint(p) should return a value, unless they're causing a side effect such as printing.

I suggest this correction:

def interp_stmt(s):
    match s:
        case Expr(Call(Name('print'), [argument])):
            print(argument)
        case Expr(value):
            return interp_exp(value)

def interp_Lint(p):
    match p:
        case Module(body):
            for s in body:
                yield interp_stmt(s)

The statement interpretation is trivial. As for the module interpretation, I think it makes sense to interpret each statement via a generator (using yield), though other solutions are viable.

jsiek commented 11 months ago

Thank you for your suggestion, but the statements in this language are intended to only cause side effects and not produce a value. This matches the behavior of Python in non-interactive mode. For example, the Expr statement is suppose to ignore the value of its subexpression, so the return that you've inserted in that case is incorrect.

AvihooI commented 11 months ago

It doesn't seem right, because interp_exp(value) wouldn't do anything. It has no side effects, it simply evaluates the expression and returns None.

Consider this test case:

def test_statement():
    statement = parse("1+2+3").body[0]
    returned_value = interp_stmt(statement)
    assert (returned_value == 6)

Without interp_stmt() returning anything, this test won't pass.

If you look at the code provided for interp_exp():

def interp_exp(e):
    match e:
        case BinOp(left, Add(), right):
            left = interp_exp(left)
            right = interp_exp(right)
            return add64(left, right)
        case BinOp(left, Sub(), right):
            left = interp_exp(left)
            right = interp_exp(right)
            return sub64(left, right)
        case UnaryOp(USub(), v):
            return neg64(interp_exp(v))
        case Constant(value):
            return value
        case Call(Name('input_int'), []):
            return input_int()

It is doing the right thing by always returning when there's no side effect (this is a slightly modified version of the code provided in the book in terms of readability, but there are no functional differences).

Can you please take a glance at it?

AvihooI commented 11 months ago

Never mind, I think I get it now. Evaluating an expression may also cause side-effects which is where this can be useful. Sorry for the bother.

jsiek commented 11 months ago

Yes, this is why the Lint and the rest of the languages also include a print statement, so that programs can produce useful output.