Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

13a Functions #25

Merged
merged 22 commits into from
Apr 22, 2022
Merged

13a Functions #25

merged 22 commits into from
Apr 22, 2022

Conversation

ngjunsiang
Copy link
Contributor

@ngjunsiang ngjunsiang commented Apr 22, 2022

The previous chapter, Procedures, introduced us to many new concepts that we will need here: use of a local frame, as well as passing by value.

The 9608 pseudocode reference guide says the following for functions:

Functions operate in a similar way to procedures, except that in addition they return a single value to the
point at which they are called. Their definition includes the data type of the value returned.

A procedure with no parameters is defined as follows:

FUNCTION <identifier> RETURNS <data type>
 <statements>
ENDFUNCTION

A procedure with parameters is defined as follows:

FUNCTION <identifier>(<param1>:<datatype>,<param2>:<datatype>...) RETURNS <data type>
<statements>
ENDFUNCTION

The keyword RETURN is used as one of the statements within the body of the function to specify the value to
be returned. Normally, this will be the last statement in the function definition.

Because a function returns a value that is used when the function is called, function calls are not complete program statements. The keyword CALL should not be used when calling a function. Functions should only be called as part of an expression. When the RETURN statement is executed, the value returned replaces the function call in the expression and the expression is then evaluated.

Example – definition and use of a function

FUNCTION Max(Number1:INTEGER, Number2:INTEGER) RETURNS INTEGER
    IF Number1 > Number2
      THEN
        RETURN Number1
      ELSE
        RETURN Number2
    ENDIF
ENDFUNCTION
OUTPUT "Penalty Fine = ", Max(10,Distance*2)

In principle, parameters can also be passed by value or by reference to functions and will operate in a similar way. However, it should be considered bad practice to pass parameters by reference to a function and this should be avoided. Functions should have no other side effect on the program other than to return the designated value.

What this means for us:

  • We'll have to figure out how to parse a FUNCTION statement
  • We'll have to implement a RETURN statement
  • We'll have to type-check function returns with their declared type
  • We'll have to figure out how to evaluate() a function as an expression with a resulting value, instead of execute()`ing it as a statement
  • We'll assume function parameters can only be defined BYVALUE, and treat any attempts to define them BYREF as a ParseError

That's four mouthfuls to chew. One at a time.

@ngjunsiang
Copy link
Contributor Author

Parsing function statements

Much of the work has been done in parsing procedures. We'll use most of that code, take out the BYREF/BYVALUE handling.

And we add in some code to handle the RETURNS:
https://github.com/nyjc-computing/pseudo/blob/283723f8ba535d4e2f46d9d5ce74d51b66748e0e/parser.py#L361-L365

And add that to our function stmt:
https://github.com/nyjc-computing/pseudo/blob/de19a53a2bd6eeb73275c87bb3dcc9da3d92a0e1/parser.py#L371-L378

@ngjunsiang
Copy link
Contributor Author

Parsing RETURN statements

RETURN statements aren't too hard to parse:
https://github.com/nyjc-computing/pseudo/blob/8b6e326bfb4a067d70f637bc61ae185668d5ace1/parser.py#L381-L388

Let's test that:

DECLARE Five : INTEGER
FUNCTION AddOne(Num : INTEGER) RETURNS INTEGER
    RETURN Num + 1
ENDFUNCTION
Five <- 5
OUTPUT "5 + 1 is ", AddOne(Five)

Result:

Expected \n

We haven't implemented parsing function calls yet ...

@ngjunsiang
Copy link
Contributor Author

Parsing function calls

Functions can be called anywhere a value or expression is expected. This is parsed by value(); that's the natural place to start adding code to handle function calls.

The first token in a function call is the function name: this is a 'name'-type token, which gets parsed as a get expression:
https://github.com/nyjc-computing/pseudo/blob/17826c09512831a37e47811cae4c927c8464f515/parser.py#L53-L56

Let's check for a '(' after that; we also parse any args, and then expect a ')' (most of this code is repeated from callStmt()):
https://github.com/nyjc-computing/pseudo/blob/17826c09512831a37e47811cae4c927c8464f515/parser.py#L57-L64

Hmm ... what kind of expression should we return?

@ngjunsiang
Copy link
Contributor Author

Function expressions

We made an early decision to represent expressions as binary expressions, with an 'oper', 'left', and 'right'. Should we stick with this format or make a new type of expression?

A get operator won't be enough for us to evaluate() a function value. And our interpreter is going to return oper(left, right) in evaluate()...should we make acall()operator? But this operator would need to know aboutexprs and stmts, so putting it in builtin.py doesn't feel appropriate.

It looks like we do need a new kind of expression after all; a call expression. It's kind of difficult to think about how to evaluate() a call expression before we've even resolve()d a FUNCTION statement. Let's do that first.

@ngjunsiang
Copy link
Contributor Author

ngjunsiang commented Apr 22, 2022

Resolving FUNCTION statements

This is going to look very similar to resolving PROCEDURE statements, but with 'passby' always being 'BYVALUE', and with an extra 'returns' key:
https://github.com/nyjc-computing/pseudo/blob/857be1acad4643fd4da7022d8c379c136058f3e2/resolver.py#L145-L165

But there's a hitch: the 'type' shouldn't be 'function'; it should be whatever our function was going to return when it gets evaluate()d! We don't know that in advance, which is why functions are declared with a RETURNS value. Let's use that as the 'type':
https://github.com/nyjc-computing/pseudo/blob/aaaac2a242603b2267add29ab8c4c0411b57dbe0/resolver.py#L156-L164

Now our function will get resolve()d properly as its return type, rather than as 'function'.

Let's get back to parsing function calls.

@ngjunsiang
Copy link
Contributor Author

Call expressions

A call involves a function, and its arguments; that's two things. Still feels very binary. It would be a waste to make a new kind of expression, with all the complexity that adds.

Let's make a simple call operator that doesn't need to know about tokens, functions, or statements:
https://github.com/nyjc-computing/pseudo/blob/f70800dd08cfcee745fff04c172500e610168cba/builtin.py#L46-L47

And construct a function call as a binary expression with a call operator:
https://github.com/nyjc-computing/pseudo/blob/4d4f0d88bdea02ef26f9f5068b318c777250f51f/parser.py#L60-L68

'left' is a get expression for the function, and 'right' is the list of arguments. (resolve() is not going to like that list of args ...)

@ngjunsiang
Copy link
Contributor Author

Resolving call expressions

Our function expression is going to be handled by resolve() in the resolver, so let's make it work.
https://github.com/nyjc-computing/pseudo/blob/947b5f0fdc053fa41360c751a66541b2d15aa8a4/resolver.py#L26-L32

Our work in [aaaac2a] made this really easy.

@ngjunsiang
Copy link
Contributor Author

Executing function declarations

Hah! That's done in the resolver already:
https://github.com/nyjc-computing/pseudo/blob/55bdc42bf3c6545712f211fb331c10e149a83e88/interpreter.py#L69-L70

@ngjunsiang
Copy link
Contributor Author

Evaluating function calls

Okay this is the tricky part. Up to this point, evaluate() has always been able to just return oper(left, right). But that's not going to be the case for call(); the poor statement trying to evaluate() a call expression would end up with a func, args tuple.

So let's add some conditional evaluation:
https://github.com/nyjc-computing/pseudo/blob/f46fbcc1857139fd3fa85daf44e57c627f2dac64/interpreter.py#L17-L22

Hold on ... we forgot about RETURN statements! (We need to type-check RETURN statements against the function RETURNS.)

@ngjunsiang
Copy link
Contributor Author

Resolving return type

It's easy enough to get the return type with the help of resolve():
https://github.com/nyjc-computing/pseudo/blob/99a3466d5fc801bc9606f42febe0e278261ffad0/resolver.py#L172-L176

We don't expect to be getting RETURN statements anywhere but in a function, so we can assume local as the frame. We resolve() the return expression to get its type.

Now we need to capture that return type and use it somewhere:
https://github.com/nyjc-computing/pseudo/blob/f44c8ca5cca33c519ba08c7e90ac39661e5d134e/resolver.py#L204-L205

Notice that this is the only verifier that returns something. Everything else returns None. Let's type-check that return value within verifyFunction():
https://github.com/nyjc-computing/pseudo/blob/60898e91010d9176ba3aba517532c4c7a71b5c4e/resolver.py#L160-L163

Function calls are now handled in evaluate().
Any return value from execute()ing function stmts
are captured and returned.
@ngjunsiang
Copy link
Contributor Author

@ngjunsiang
Copy link
Contributor Author

Testing and bugfix

We forgot to resolve() function call args: [56dc061]

And we also forgot to return the return value in execute(): [8a8d166]

Test code:

DECLARE Five : INTEGER
FUNCTION AddOne(Num : INTEGER) RETURNS INTEGER
    RETURN Num + 1
ENDFUNCTION
Five <- 5
OUTPUT "5 + 1 is ", AddOne(Five)

Result:

5 + 1 is 6

Annnnnd ... we have working functions 🙌

@ngjunsiang ngjunsiang merged commit c6eb333 into main Apr 22, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

1 participant