How does --trace work?
The structure resembles a tree where each node has properties: name, execution_time and children
Two global variables are important: current_frame and trace_root
trace_rootmarks the entry point into the tree is in the beginning always set to:{"name": "main", "execution_time": None, "children": []}⇒ makingmainthe frame at depth 0 and marking all successive calls as children ofmaincurrent_framepoints to thechildrenarray of the current frame
If the global flag TRACING_ENABLED is set, do_call and do call set_frames , passing the name of the called function
- In the case of
do(non-user-defined functions), it is checked whether the function is whitelisted or not (onlyprintis whitelisted)
set_frames constructs a new node for the tree (only explicitly setting name because time is not yet known and new node does not (yet) have children)
- It appends the new node to
current_frame - It stores
current_frameand overrides it with the (empty)childrenproperty of the new node ⇒ Allows for nested calls - It returns the new node plus the copy of the overwritten
current_frame
do and do_call calculate and set the execution time of the new node
After the call has finished, reset_frames is called with the “last” current_frame, resetting the global current_frame to that pointer
Explain this code snippet
def parse_args(argv=None):
p = argparse.ArgumentParser()
p.add_argument(
"--trace",
action="store_true",
help="Print a function-call trace with timings",
)
p.add_argument("--log_envs", action="store_true", help="Print envs at end of program")
p.add_argument("filename", help="Program file to run (.lgl)")
return p.parse_args(argv)-
argsparse.ArgumentParser()builds an object that knows how to read and validate CLI arguments. It automatically providesh/--helpoutput. -
Three arguments are defined
--traceand--log_envsboth of which are optional- The
action=store_truesetsargs.traceandargs.log_envstoTrueorFalsedepending whether or not the flags were provided
- The
- Positional argument
filenamewhich is not optional (noaction=store_trueprovided)-
Positional arguments are provided without leading dashes
-
They are identified by their position in the program call whereby non positional arguments do not count towards their position:
python interpreter.py --trace hello.lglpython interpreter.py hello.lgl --traceare equivalent
-
-
Finally, the return value of
p.parse_args(argv)is used to set global flagsTRACING_ENABLEDandLOG_ENVS
How is scoping handled in the interpreter?
Structure: Environment variables are tracked via a stack of dicts called envs
Scoping is done dynamically: envs_get finds the most recent definition of a variable in the stack ⇒ this means that functions can access variables which lexically are outside their scope. You could say that every defined variable (outside the call of a user-defined function) is global and lives forever. For example: everything defined inside a seq or a while will add to the global envs dict and live there forever while in lexical scoping, variables defined inside these blocks would be lost after they have finished
- BUT there is a scoping mechanism built in: when calling user defined functions, a new frame is added to
envswhich only is available during the function call (popped afterwards). This means that labels and their values inside the body of user defined functions are only visible during their execution- But as long as the function is running, the variables are visible, so nested functions can see the parameters of their parent functions
⇒ globals persist for the whole run; locals (defined inside user-defined functions) persist only for the duration of a call; and name lookup is dynamic across the call stack (= simply finding most recent implementation)
Explain this line: op = ALIASES.get(program[0], program[0])
- This is a fallback ⇒ If
program[0]is present inALIASES, the respective value for they key is assigned toop - If the key
program[0]is not present inALIASES,op = program[0]
Explain this block at the top of do:
try:
return env_get(program, envs)
except AssertionError:
passIt allows for evaluating strings as variable names:
# This works (if x was previously defined)
["add", "x", 1]
# Makes this redundant:
["add", ["get", "x"], 1]-
Important: Only bare strings are evaluated! One could think that because the block is at the top of
do, whole expressions like["add", "x", 1]could be looked up in envs (which would be really bad), this is not true becauseenvs_getasserts thatprogramis a bare string -
But our implementation does have a problem:
envs_getreturns an assertion error in two cases:- The passed in parameter is not a string
- The string was not found in envs, meaning that the variable was not defined
-
But our implementation catches and suppresses both assertion errors, so there is no distinction between variable not found (breaking, program should stop) and looking up a non string literal (expected)
- This means that if we do something like this:
# Hello not wrapped inside quote ["print", "hello"]
- The program looks up “hello” in envs and does not find it → we suppress the raised error by
envs_getand searchprogram[0]in theOPSdict (we have a missingassert(program, list)guard there) ⇒ Program will error with “Unknown operation ‘h’” becauseprogram[0] == "h" - This is unexpected behaviour. What we should’ve done is:
# Only evaluate string literals if isinstance(program, str): return env_get(program, envs) # No catch: not found variables should raise!
Explain the need for quote in the code
Quote allows strings to be treated as data rather than variable names. For example: because we look up all string literals in do at the beginning, "hello" is evaluated as a variable name in ["print", "hello"]
- With quote, we can do:
["print", ["quote", "hello"]]and treat “hello” as a normal string