# ---
# jupyter:
#   jupytext:
#     text_representation:
#       extension: .py
#       format_name: light
#       format_version: '1.5'
#       jupytext_version: 1.14.4
#   kernelspec:
#     display_name: Python 3 (ipykernel)
#     language: python
#     name: python3
# ---

# # Python Bytecode and Disassembly
# ## Compiling Python Code and Code Objects
# Usually the difference between compiled and interpreted languages is that a compiled language must be converted from high-level code to CPU instructions whereas interpreted languages read code line by line and execute it. However, Python is not fully interpreted like shells are nor is it compiled in the traditional sense we know. Instead Python source code is compiled to a simpler instruction set called bytecode which is executed by the Python Virtual Machine. This compilation step is usually performed at runtime as needed and is why you may `.pyc` files in a `__pycache__` folder after execution. This is where Python caches is compilation to use next execution if the code remains unchanged. It is possible to compile your Python files before execution using `python -m compileall [file]` and `.pyc` files can be run like normal Python files using `python [pyc_file]`.
#
# To compile a string of Python code to a code object we can use the built in `compile` function. This gives us a code object which has different attributes like `co_code` which contains the bytecode in byte form, `co_consts` which contains all constants, and `co_varnames` which contains a list of variable names used in the code. Notice how any methods, classes, or lambda expressions within your code is compiled to seperate code objects stored within `co_consts`.

code = "print('hello world!')"
code = compile(code, 'test', 'exec')
code

code.co_code

code.co_consts

eval(code)

# + [markdown] tags=[]
# ## Disassembling Code
# [`dis`](https://docs.python.org/3/library/dis.html) is a built-in module for disassembling CPython bytecode. The built in `dis.dis` method prints out the disassembly of any code object passed to it.

# + tags=[]
import dis
dis.dis(code)
# -

# This disassembly format may be hard to read at first but from left to right is the line number in the code, byte offset, instruction name, and arguments. Python bytecode is run in a stack-based virtual machine and is oriented entirely around stack based data structures.
# ### Common Bytecode Instructions
# Here are some of the common instructions you'll see in Python Bytecode
# #### `POP_TOP`
# Bytecode is run in a stack and this instruction removes the item at the top of the stack (usually refered to as top-of-stack or TOS)
# #### `LOAD_NAME`
# Pushes the value associated with the name onto the stack. Takes a single name argument.
# #### `LOAD_CONST`
# Pushes the constant argument onto the stack.
# #### `LOAD_GLOBAL`
# Pushes the value associated with the global name argument onto the stack. Works similarily to `LOAD_NAME`.
# #### `STORE_NAME`
# Stores the TOS item to the variable name given in argument. Removes the TOS from stack.
# #### `STORE_FAST`
# Stores the TOS item to the variable name given.
# #### `CALL_FUNCTION`
# Calls a callable/function item with positional arguments. The argument for this instruction is the number of positional arguments should be passed to the function from the TOS. Below the arguments is the callable item. This instruction will pop all the positional arguments and callable item off the stack and push the return value of the function.
# #### `LOAD_METHOD`
# Loads a method item with name matching argument from the TOS item. TOS is popped and the object and method are pushed to TOS if it is an unbound method. Otherwise, `NULL` and the method are pushed to TOS.
# #### `CALL_METHOD`
# Calls a method with positional arguments. The argument for this instruction is number of positional arguments passed from TOS. Below the arguments is the callable method and method object. Arguments, method, and method object are poped and the return value is pushed to TOS. This instruction is designed to be used with the `LOAD_METHOD` instruction.
# #### `RETURN_VALUE`
# Returns the TOS item to the caller of the function.

# ### Math Instructions
# #### Binary Operations

code = """
a = 2
b = 3
sum = a + b
difference = a - b
product = a * b
quotient = a / b
floored = a // b
power = a ** b
"""
code = compile(code, 'test', 'exec')
dis.dis(code)

# Binary operation instructions remove the TOS and second most top-of-stack item (TOS1), perform the operation, and push the result back onto the stack.
#
# `BINARY_ADD`: `TOS = TOS + TOS1`
#
# `BINARY_SUBTRACT`: `TOS = TOS - TOS1`
#
# `BINARY_MULTIPLY`: `TOS = TOS * TOS1`
#
# `BINARY_TRUE_DIVIDE`: `TOS = TOS / TOS1`
#
# `BINARY_FLOOR_DIVIDE`: `TOS = TOS // TOS1`
#
# `BINARY_POWER`: `TOS = TOS ** TOS1`
#
# `BINARY_MODULO`: `TOS = TOS % TOS1`
#
# `BINARY_XOR`: `TOS = TOS ^ TOS1`
#
# `BINARY_AND`: `TOS = TOS & TOS1`
#
# `BINARY_OR`: `TOS = TOS | TOS1`
#
# #### Inplace Operations

code = """
a = 13
b = 15
a += b
b -= b
a *= b
a /= b
"""
code = compile(code, 'test', 'exec')
dis.dis(code)

# Inplace operation instructions are used when there for inplace operators in Python code. Like binary operation instructions, they pop TOS and TOS1, perform the operation, and push the result onto the stack.
#
# `INPLACE_ADD`: `TOS += TOS1`
#
# `INPLACE_SUBTRACT`: `TOS -= TOS1`
#
# `INPLACE_MULTIPLY`: `TOS *= TOS1`
#
# `INPLACE_POWER`: `TOS **= TOS1`
#
# `INPLACE_TRUE_DIVIDE`: `TOS /= TOS1`
#
# `INPLACE_FLOOR_DIVIDE`: `TOS //= TOS1`
#
# `INPLACE_MODULO`: `TOS %= TOS1`
#
# `INPLACE_XOR`: `TOS ^= TOS1`
#
# `INPLACE_AND`: `TOS &= TOS1`
#
# `INPLACE_OR`: `TOS |= TOS1`
#
# #### Unary Operation Instructions
# These operation instructions take the TOS, apply an operation, and push the result back onto the stack.
#
# `UNARY_POSITIVE`: `TOS = +TOS`
#
# `UNARY_NEGATIVE`: `TOS = -TOS`
#
# `UNARY_NOT`: `TOS = not TOS`
#
# `UNARY_INVERT`: 

# ### Comparison Instructions

code = """
1 == 1
1 != 0
1 <= 0
1 in [1,2,3]
a = 'a'
b = a
b is a
"""
code = compile(code, 'test', 'exec')
dis.dis(code)

# #### `COMPARE_OP`
# Takes a comparison operator as an argument, takes the TOS and TOS1, performs the comparison on them, and pushes the True or False result onto the stack
# #### `IS_OP`
# Has a single invert argument, takes the TOS and TOS1, performs a `is` comparison on them, unless the invert argument is set to 1 where the instruction performs a `is not` comparison. Pushes the result onto the stack
# #### `CONTAINS_OP`
# Has a single invert argument, takes the TOS and TOS1, performs a `in` comparison on them, unless the invert argument is set to 1 where the instruction performs a `not in` comparison. Pushes the result onto the stack
# ### Conditionals

code = """
num = 5
if num % 2 == 0:
    print('even')
else:
    print('odd')
"""
code = compile(code, 'test', 'exec')
dis.dis(code)

# `if`, `elif`, and `else` statements in Python bytecode are done using the `POP_JUMP_IF_FALSE` and `POP_JUMP_IF_TRUE` instructions. The conditional blocks end with a `JUMP_FORWARD` or `JUMP_ABSOLUTE` instruction to jump to the next instruction outside of conditional blocks. Jump targets are easy to find in the disassembler's output as they are marked by a `>>` before the bytecode offset.
# #### `POP_JUMP_IF_FALSE`
# If TOS is false jumps to bytecode instruction at target argument. Pops TOS.
# #### `POP_JUMP_IF_TRUE`
# If TOS is true jumps to bytecode instruction at target argument. Pops TOS.
# #### `JUMP_FORWARD`
# Increments the bytecode counter by the delta argument.
# #### `JUMP_ABSOLUTE`
# Jumps to the bytecode instruction at target argument.
# ### While Loops

code = """
lst = [1,2,3,4,5]
while len(lst) > 0:
    lst.pop()
"""
code = compile(code, 'test', 'exec')
dis.dis(code)

# While loops function very similarily to `if` statements in Python bytecode. The major difference is that the end of the loop block has a `JUMP_ABSOLUTE` instruction that jumps back to the comparison to test if the loop should continue to run.
# ### For Loops

code = """
for i in range(0,10,2):
    print(i)
"""
code = compile(code, 'test', 'exec')
dis.dis(code)

# For loops use a `GET_ITER`instruction to get an iterator from an object, a `FOR_ITER` instruction to grab the next value from the iterator, and a `STORE_NAME` instruction to store the value to the loop variable. The loop ends with a `JUMP_ABSOLUTE` instruction to jump to the `FOR_ITER` instruction at the end of the loop block's execution.
# #### `GET_ITER`
# Implements `TOS = iter(TOS)`
# #### `FOR_ITER`
# Grabs the next value from the iterator at TOS and pushes it onto the stack. If the iterator is exhausted, the bytecode counter is incremented by the delta argument.

StarcoderdataJupyterScriptsDedupFiltered