Hello, dear friend, you can consult us at any time if you have any questions, add WeChat: YSYY66
Project #2: Brewin++
Introduction
The Brewin standards body has met and has identified a bunch of new improvements to the Brewin language - so many, in fact that they want to update the language’s name to Brewin++. In this project, you will be updating your Brewin interpreter so it supports these new Brewin++ features. As before, you’ll be implementing your interpreter in Python, and you may solve this problem using either your original Project #1 solution, or by modifying the Project #1 solution that we provided (see below for more information on this).
Once you successfully complete this project, your interpreter should be able to run syntactically- correct Brewin++ programs.
So what new language features were added to Brewin++? Here ’ s the list:
Variable and Typing Changes
Brewin++ now implements static typing1 for all variables. All variables now must be defined before they are used (like in C++), and a type must be specified in each definition. In addition, the language must ensure that all variable assignments and expressions have compatible types;
no implicit conversions are allowed.
Here ’ s an example program:
func main void
var int b c d # b,c, and d are integer variables
assign b 5
var bool e # e is a boolean variable
assign e "foobar" # this would be a type error!
endfunc
Scoping Changes
Brewin++ now implements lexical scoping for all variables, just like you’d see in C++. Here’s an example program:
func main void
var int a
assign a 5
if > a 0
funccall print a # prints 5
var string a # legal: shadows our original a variable
assign a "foobar"
funccall print a # prints foobar
var bool b
endif
funccall print a # prints 5
funccall print b # Name error: b is out of scope
endfunc
Function Changes
Brewin++ now supports parameters for user-defined functions, and allows arguments to be passed to these functions by value or by reference. Functions also now must have an explicit return type. To access the return value from a function, you no longer refer to the result variable, but refer to resulti for integers, resultb for booleans, and results for strings.
Here ’ s an example program:
# Equivalent to: bool absval(int val, int& change_me)
func absval val:int change_me:refint bool
if < val 0
assign change_me * ‐1 val
return True
else
val
endif
endfunc
func main void var int val output assign val ‐ 5 funccall absval val funccall print "The funccall print "Did
endfunc
output
absolute value is: " output
I negate the input value? " resultb
Brewin++ Language Detailed Spec
The following sections provide detailed requirements for the Brewin++ language so you can implement your interpreter correctly. Other than those items that are explicitly listed below, all other language features must work the same as in the original Brewin v1 language. As with Project #1, you may assume that all programs you will be asked to run will be syntactically correct (though perhaps not semantically correct). You must only check for the classes of errors specifically enumerated in this document, although you may opt to check for other (e.g., syntax) errors if you like to help you debug.
Variable Changes: Definition and Typing
● Variables now must be defined before they are used (like C++), and a type must be specified in the definition
○ Valid variable types are: int, bool, string
○ More than one variable may be defined at a time on a line
○ The syntax is:
var type variable1 variable2 … variablen
Examples:
var int alpha beta gamma # defines alpha, beta and gamma as ints var string delta # defines delta as a string
If an undefined variable is referred to in any part of your program (e.g., you try to print its value out, use it in an expression, or assign it a value and it was not explicitly defined as a formal parameter or a variable), you must report an error when interpreting the line by
calling the InterpreterBase.error() method with an error type of
ErrorType.NAME_ERROR.
● The value given to a newly-defined variable, whose value has not yet been explicitly assigned, is the default value for the type, e.g.:
○ int: 0
○ bool: False
○ string: “ ”
● The language must ensure that all variable assignments and expressions have compatible types; no implicit conversions are allowed. If the types in an expression or assignment are not compatible, you must generate an error when interpreting the line by calling InterpreterBase.error() with a type error of ErrorType.TYPE_ERROR.
Examples: var int a var bool b
# defaults to zero
# defaults to false
assign a b # type error; no conversion allowed between bool to int assign b 1 # type error, no conversion allowed between int to bool assign a * 3 b # type error, booleans can’t be multiplied with integers
Variable Changes: Scoping
● Variables are scoped to the function in which they are defined, and are not global (and therefore not visible to other functions). When a function ends, all variables defined in a function go out of scope and their lifetime ends, e.g.:
func foo void
# error ‐ blah is NOT known to foo(), only to main()
funccall print blah
endfunc
func main void
var int blah
funccall foo
endfunc
● Variables are scoped to the block in which they are defined (and are visible in nested blocks within the block where they are defined); when a block ends, all variables defined in that block go out of scope and their lifetime ends.
func main void
var int a
assign a 5
if > a 0
funccall print a # this is valid; a is visible to inner block
var string b # b is scoped to the if ‐ statement block
endif
funccall print b # Name error: b is out of scope
endfunc
● Duplicate variable definitions within the same block are not allowed.
func main void
var int a
assign a 42
var int a # this is illegal!
endfunc
If a variable is redefined in the same block, you must report an error when interpreting the line by calling the InterpreterBase.error() method with an error type of ErrorType.NAME_ERROR.
● An inner/nested block may define a variable of the same name as a variable in an enclosing block, and that definition will shadow (hide) the outer definition until the end of the current block, when the inner variable disappears, e.g.:
func main void
var int a
assign a 5
if > a 0
funccall print a # prints 5, since a is visible from outer scope
var string a # new a variable shadows our original a
assign a "foobar"
funccall print a # prints foobar
endif
funccall print a # prints 5, since inner ‐ foobar is out of scope
endfunc
● The scope of all results returned by a function must be scoped to the top level of the calling function, e.g.:
func bar int
# code to compute some value
return some_val
endfunc
func foo void
if True
funccall bar
funccall print resulti endif
funccall print resulti
endfunc
# resulti is set upon return from bar
# valid since resulti is in scope
# also valid since resulti has top-level scope
● resulti, resultb, or results must only be defined if they are explicitly set by a called function that returns a value. So this would be invalid:
func foo void
funccall print resulti # we never called a function, so we can’t refer to resulti endfunc
You must report an error when interpreting the line by calling the
InterpreterBase.error() method with an error type of ErrorType.NAME_ERROR.
Note : Calling a function which returns one type should not affect the result variables of the other types. For example, calling a function which returns an integer, should not affect resultb or results.
Function Changes: Parameter Passing
● Functions now may be defined with zero or more typed parameters which may be passed by value or by reference. Functions must also have an explicit return type. The syntax for a function header is:
func funcname var1 :type1 var2 :type2 … varn :typen return_type
Where parameter types may be: int, bool, string, refint, refbool, refstring Where the return types may be: int, bool, string, void
We guarantee that there will be no spaces in between the parameter name, the colon,
and the type name.
Example function headers:
func foo xyz:int pdq:refbool void
func bar int
func bletch pqr:refstring void
● Type compatibility:
○ You may pass an int/bool/string variable to a function that accepts a refint/refbool/refstring parameter, respectively.
○ You may pass an refint/refbool/refstring variable to a function that accepts a refint/refbool/refstring parameter, respectively.
○ You may pass an refint/refbool/refstring variable to a function that accepts an int/bool/string parameter, respectively.
○ If an actual parameter’s type and a formal parameter’s type are not compatible, you must generate an error when interpreting the function call line by calling InterpreterBase.error() with a type error of ErrorType.TYPE_ERROR.
● Changes to parameters:
○ Parameters may have their value changed within a function. If the parameter is of a reference type, then the assignment will modify the original referred-to variable’s value. If the parameter is passed by value, then the assignment will change the local variable associated with the parameter and must not modify the caller’s variable that was passed in.
■ Note: You must consider the case where we pass a variable x to a function that accepts a reference y, then pass y to another function that accepts a reference z, etc. Modifying parameter z must result in the original variable, x, being modified.
○ It is legal to pass a constant value to a function that accepts a reference. In such a case, if the reference parameter is reassigned, this change will only be reflected within the current function, since the original constant value can’t be changed.
Function Changes: Returning Values
● All functions must have an explicit return type of int, bool, string or void, e.g.:
func foo arg:int void
endfunc
func bar a:string b:bool int
endfunc
● When returning an expression/value from a function, the type of that value being returned must be consistent with the return type of the function.
○ If the type being returned is incompatible with the return type of the function, then you must generate an error when interpreting the return line by calling InterpreterBase.error() with a type error of ErrorType.TYPE_ERROR.
○ If the return command is used in a function that returns an int, bool or string (not void), and a return value/expression is not specified, then the default value for the type (0 for int, False for bool, “” for string) must be returned to the caller.
For example, the following are invalid:
func foo void
return 5 # error
endfunc
func bar int
return “ blah ” # error
endfunc
For example, the following are valid:
func foo string
return # ok; returns “” default value
endfunc
func foo int
return 5 # ok
endfunc
func foo arg:refint int
return arg # refint and int are compatible endfunc
Note : Specifying no return statement is equivalent to a
default return
● While in Brewin v1, results were always returned in a variable called “ result”, in Brewin++ results are delivered via three separate result variables:
○ Upon returning from a function f() that returns an integer result, the return value of f() must be placed in a variable named resulti
○ Upon returning from a function f() that returns a boolean result, the return value of f() must be placed in a variable named resultb
○ Upon returning from a function f() that returns a string result, the return value of f() must be placed in a variable named results
● All results (resulti, resultb, results) must scoped to the top-level scope of the calling function, to enable use cases like this:
func foo arg:int void
if > arg 5
funccall input "Enter your name: "
else
funccall input "Enter your dog ’ s name: "
endif
# notice how results is referred to outside the block where # we called input to get the name.
funccall print "You entered: " results
endfunc
func main void
funccall foo 5
endfunc
Expressions and Assignment Changes
● During expression evaluation, the interpreter must validate that the variables’ types (as specified in their definition) are compatible with their operators, and with the types of other variables/constants within the expression.
Note: It is acceptable to use a refint/refbool/refstring variable in an expression that otherwise is operating on int/bool/string items, e.g.:
func foo arg:refint void
var int b
assign b * arg 5 # arg is a refint, 5 is an int, this is OK
endfunc
● During assignment, the interpreter must validate that the variable having its value changed has a compatible type with the expression/value that it’s being set to, e.g.
var string a
var int b
assign b a # this is an error, since b is not a string
Note : it is acceptable to assign a refint variable to an int, and visa-versa, e.g.:
func foo arg:refint void
assign arg 5 # arg is a refint, 5 is an int, and arg can be assigned to int
var b int
assign b arg # b is an int, arg is a refint, and be can be assigned to arg endfunc
● If any types within an expression or an assignment are invalid, then you must generate an error when interpreting the line by calling InterpreterBase.error() with a type error of ErrorType.TYPE_ERROR.
Error Checking Requirements
Error checking stay the same as in Project #1 - you must only handle the errors detailed explicitly in this spec.
Things you will not have to worry about:
● Any syntax errors. You can assume that all programs will be syntactically correct.
● You will never have any nested double quotes inside strings in Brewin++. You may have single quotes inside double quotes.
● We will never call strtoint on a non-integral value
● The program will always have valid indentation.
● You will never have variables with the same names as reserved keywords, e.g. func, int, etc. You may have variables named as resulti/resultb/results however.
● You can assume that we will not redefine a result variable to be of a different type. For example, we will never have the statement var bool resulti in any of our tests.
Coding Requirements
Coding requirements are the same as Project #1, with the exception of:
● You must name your interpreter source file interpreterv2.py
● We will not increase the default recursion limit in Python . Your code must not hit the recursion limit .
○ To compensate, we will provide you the “ most recursive” test case; if it passes this test case, it should work properly on the hidden cases.
While this is not required, we have a handful of suggestions to make your code play nicely with the autograder:
● Don ’ t include code that runs globally; wrap your main routine in an if-guard
● Don’t use exit() or any other code that could abruptly terminate your program; instead, use our provided error tools in intbase
● Don’t modify signal handlers (if you don’t know what this means, you’re probably not doing it)
Deliverables
For this project, you will turn in at least two files:
● Your interpreterv2.py source file
● A readme.txt indicating any known issues/bugs in your program
● Other python source modules that you created to support your interpreterv2.py module (e.g., environment.py, type_module.py)
Whatever you do, make sure to turn in a python script that is capable of loading and running, even if it doesn’t fully implement all of the language’s features. We will test your code against dozens of test cases, so you can get substantial credit even if you don’t implement the full language specification.
The TAs have updated the template GitHub repository : it now contains intbase.py, a copy of Carey’s fully working solution for Project 1, as well as a brief description of what the deliverables should look like.
Grading
Your score will be determined entirely based on your interpreter’s ability to run Brewin++ programs correctly, however you get karma points for good programming style. A program that doesn’t run with our test automation framework will receive a score of 0%.
The autograder we are using, as well as a subset of the test cases, is publicly available on GitHub . Other than additional test cases, the autograder is exactly what we deploy to Gradescope. Students are encouraged to use the autograder framework and provided test cases to build their solutions.
New from Project 1: we now give you your entire mark on every submission.
● 20% of the test cases are provided in the autograder repo .
● 80% of the test cases are hidden from you until grades are published. However, you’ll see your score on these test cases on Gradescope.
There are three stages to the project; students are currently at the second. Thus, this folder contains the necessary bootstrapping code:
intbase.py, the base class and enum definitions for the interpreter
a sample readme.txt, which should illustrate any known bugs/issues with the program
(or, an "everything's good!")
As well as canonical solutions for Project 1 (written by Carey):
interpreterv1.py: a top-level entrypoint: has some utility classes, finding the main
function, the interpreter loop, and handlers for each token type
env_v1.py: manages the "environment" / state for a program
func_v1.py: manages and caches functions (will be much more useful in Project 2!)
tokenize.py: tokenization logic
You do not have to use the canonical solutions for Project 2; in particular, since you didn't write the code, it may be confusing!
Some notes on your submission (for Project 2; we'll update this for later projects):
1. You must have a top - level , versioned interpreterv 2. py file that exports the Interpreter class . If not, your code will not run on our autograder .
2. You may also submit one or more additional .py modules that your interpreter uses, if you decide to break up your solution into multiple .py files.
You can find out more about our autograder, including how to run it, in its accompanying repo .
Using this repository / testing locally is entirely optional . It does not directly affect your grade. You are free to only submit to Gradescope !
This repository contains:
the full source code for the autograder we deploy to Gradescope
20% of the test cases we evaluate your code on; these are the test cases that are public
on Gradescope
o testsv1 contains source ( .src), expected ( .exp), and standard input ( .in) files for programs that should interpret and run without errors
o failsv1 contains source ( .src), expected ( .exp), and standard input ( .in) files for programs that should interpret successfully, but error
canonical solutions for the past projects:
o Carey's solution for Project 1: interpreterv1.py, env_v1.py, func_v1.py,
and tokenize.py. More on this in the project template repo .
This repository does not contain:
80% of the test cases we evaluate your code on
the plagiarism checker, which is closed-source
the Docker configuration for the deployment; this is managed by Gradescope
We'll note that with the current setup, we grant five seconds for each test case to run . We've made a separate repository for project template code .
Testing Locally
To test locally, you will additionally need a working implementation of the project; the minimum example is an interpreterv1.py/interpreterv2.py that implements the Interpreter class.
Place this in the same directory as tester.py. Then, to test project 1,
$ python3 tester.py 1
Running 10 tests...
Running testsv1/test1.src... PASSED
Running testsv1/test2.src... PASSED
Running testsv1/test6.src... PASSED
Running testsv1/test8.src... PASSED
Running testsv1/test10.src... PASSED
Running testsv1/test27.src... PASSED
Running testsv1/test28.src... PASSED
Running failsv1/test1.src... PASSED
Running failsv1/test9.src... PASSED
Running failsv1/test7.src... PASSED
10/10 tests passed.
Total Score: 100.00%
Similarly, one can test version 2, which requires a interpreterv2.py, with: $ python3 tester.py 2
Running 25 tests .. .
testsv2/test2.src... PASSED
testsv2/test3.src... PASSED
testsv2/test6.src... PASSED
testsv2/test7.src... PASSED
testsv2/test8.src... PASSED
testsv2/test10.src... PASSED
testsv2/test11.src... PASSED
testsv2/test12.src... PASSED
testsv2/test13.src... PASSED
testsv2/test47.src... PASSED
testsv2/test16.src... PASSED
testsv2/test50.src... PASSED
testsv2/test53.src... PASSED
testsv2/test22.src... PASSED
testsv2/test55.src... PASSED
failsv2/test3.src... PASSED
failsv2/test4.src... PASSED
failsv2/test8.src... PASSED
failsv2/test9.src... PASSED
failsv2/test10.src... PASSED
failsv2/test20.src... PASSED
failsv2/test21.src... PASSED
failsv2/test23.src... PASSED
failsv2/test24.src... PASSED
failsv2/test27.src... PASSED
...
And version 3 with python3 tester.py 3.
The output of this command is identical to what is visible on Gradescope pre-due date , and they are the same cases that display on every submission. If there is a discrepancy, please let the teaching team know!
Note: we also output the results of the terminal output to results.json.