Repository: MarbleHE/HECO (language: python)

Issue:
ABC AST Limitations
# ABC AST Limitations

This document gathers limitations of our current AST which mainly became apparent while building the Python frontend. Some duplicates with #36 may exist.


## Multi-Return Values / Multi-Assignment

### What?
The following (Python) syntax is not supported in our AST:
```Python
def f():
  return 1, "abc", 2.0 # <- Multi-return

a, b, c = f() # <- Multi-assignment
```

Something similar is supported though: return values can be `ExpressionList`s, which contain (a fixed number of) entries of the same type. However, there is no multi-assignment. In fact, those vectors are never changed in size and we always operate on the full vector. All variables are vectors. Thus, assigning variables to parts of the vector is not (straightforward) to support.

## Why is this useful?
To efficiently translate higher level languages as Python to our framework.


## Type Casting

We do not support type casting yet. I.e., `double * int` will result in an error and not implicitly cast the `int` to a double. A real intermediate representation should solve such issues.

## No Break Statement / No While Loop

There is no `break` statement for for-loops. This is an issue for the Python frontend, since Python has limited for-loops. Instead, we often use while-loops to express things.

But without `break`, some while loops cannot easily be translated, e.g.
```Python
i = 1
while True:
  i += i
  if i > 100:
    break
```

because here, we do an update _before_ checking the condition. This is not the same as

```C
int i;
for (i = 1; i > 100; i += i);
```

E.g., when we initialize `i` to `100`, the first results in `i = 200`, the second in `i = 100`. Detecting how to transform those loops doesn't seem trivial.

Of course, this is only translatable to FHE if `i` is a non-secret value. We could emulate while loops with for loops (as `for(; true; )`) but that still requires a `break` statement for the terminating condition.

## Slices
We don't support slices, e.g. `v[3:5]` to get part of a vector. This is also cumbersome to add, since the vectors used internally never change their size.

## Tuple vs List

There is no distinction between tuples and lists in our AST. The main difference in Python is that `Tuples` are immutable while Lists are mutable. Currently, we use `ExpressionList`s for Python lists and don't translate `Tuples`. One of the disadvantages is that we cannot support return statements with multiple values (because they are tuples in Python).


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index d1240508..b3a65c6c 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -9,7 +9,7 @@ jobs:
       - name: update apt-get sources
         run: sudo apt-get update
       - name: install gcc-9
-        run: sudo apt-get install -y gcc-9 g++-9 build-essential
+        run: sudo apt-get install -y gcc-9 g++-9 build-essential libboost-all-dev python3-dev python3-pip cmake
       - uses: actions/checkout@v1
       - name: Generate short commit hash
         id: vars
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 69d904c9..944cf2ea 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -10,6 +10,16 @@ if (MSVC)
     cmake_policy(SET CMP0091 NEW)
 endif ()
 
+# Set to test the python frontend. On by default.
+if(NOT PYTHON_FRONTEND_DEV)
+    set(PYTHON_FRONTEND_DEV true)
+endif()
+
+# Set to build the python frontend (installing pybind11 and building the wrapper). On by default.
+if(NOT PYTHON_FRONTEND_BUILD)
+    set(PYTHON_FRONTEND_BUILD true)
+endif()
+
 project(ast_optimizer
         VERSION 0.1
         DESCRIPTION "Optimizer for Fully Homomorphic Encryption (FHE) working on an AST-like (Abstract Syntax Tree) representation of a program"
@@ -99,6 +109,7 @@ set(SOURCE_FILES
         ${ast_optimizer_SOURCE_DIR}/src/ast/UnaryExpression.cpp
         ${ast_optimizer_SOURCE_DIR}/src/ast/Variable.cpp
         ${ast_optimizer_SOURCE_DIR}/src/ast/VariableDeclaration.cpp
+        ${ast_optimizer_SOURCE_DIR}/src/compiler/Compiler.cpp
         ${ast_optimizer_SOURCE_DIR}/src/parser/Errors.cpp
         ${ast_optimizer_SOURCE_DIR}/src/parser/File.cpp
         ${ast_optimizer_SOURCE_DIR}/src/parser/Parser.cpp
@@ -113,6 +124,7 @@ set(SOURCE_FILES
         ${ast_optimizer_SOURCE_DIR}/src/utilities/BatchingConstraint.cpp
         ${ast_optimizer_SOURCE_DIR}/src/utilities/ComplexValue.cpp
         ${ast_optimizer_SOURCE_DIR}/src/utilities/Datatype.cpp
+        ${ast_optimizer_SOURCE_DIR}/src/utilities/NodeUtils.cpp
         ${ast_optimizer_SOURCE_DIR}/src/utilities/Operator.cpp
         ${ast_optimizer_SOURCE_DIR}/src/utilities/PlainVisitor.cpp
         ${ast_optimizer_SOURCE_DIR}/src/utilities/Scope.cpp
@@ -135,6 +147,7 @@ set(HEADER_ONLY_SOURCES
         ${ast_optimizer_SOURCE_DIR}/include/ast_opt/ast/AbstractExpression.h
         ${ast_optimizer_SOURCE_DIR}/include/ast_opt/ast/AbstractStatement.h
         ${ast_optimizer_SOURCE_DIR}/include/ast_opt/ast/AbstractTarget.h
+        ${ast_optimizer_SOURCE_DIR}/include/ast_opt/ast/Ast.h
         ${ast_optimizer_SOURCE_DIR}/include/ast_opt/runtime/AbstractCiphertext.h
         ${ast_optimizer_SOURCE_DIR}/include/ast_opt/runtime/AbstractCiphertextFactory.h
         ${ast_optimizer_SOURCE_DIR}/include/ast_opt/runtime/AbstractNoiseMeasuringCiphertext.h
@@ -142,8 +155,7 @@ set(HEADER_ONLY_SOURCES
         ${ast_optimizer_SOURCE_DIR}/include/ast_opt/runtime/Cleartext.h
         ${ast_optimizer_SOURCE_DIR}/include/ast_opt/utilities/IVisitor.h
         ${ast_optimizer_SOURCE_DIR}/include/ast_opt/utilities/VariableMap.h
-        ${ast_optimizer_SOURCE_DIR}/include/ast_opt/utilities/Visitor.h
-        )
+        ${ast_optimizer_SOURCE_DIR}/include/ast_opt/utilities/Visitor.h)
 
 # This contains the headers that developers that want to link directly against ABC might need to have access to
 # These are the headers that are installed when ABC is installed as a system-wide library
@@ -168,6 +180,7 @@ set(PUBLIC_HEADERS
         ${ast_optimizer_SOURCE_DIR}/include/ast_opt/ast/UnaryExpression.h
         ${ast_optimizer_SOURCE_DIR}/include/ast_opt/ast/Variable.h
         ${ast_optimizer_SOURCE_DIR}/include/ast_opt/ast/VariableDeclaration.h
+        ${ast_optimizer_SOURCE_DIR}/include/ast_opt/compiler/Compiler.h
         ${ast_optimizer_SOURCE_DIR}/include/ast_opt/parser/Errors.h
         ${ast_optimizer_SOURCE_DIR}/include/ast_opt/parser/Parser.h
         ${ast_optimizer_SOURCE_DIR}/include/ast_opt/parser/PushBackStream.h
@@ -181,6 +194,7 @@ set(PUBLIC_HEADERS
         ${ast_optimizer_SOURCE_DIR}/include/ast_opt/utilities/BatchingConstraint.h
         ${ast_optimizer_SOURCE_DIR}/include/ast_opt/utilities/ComplexValue.h
         ${ast_optimizer_SOURCE_DIR}/include/ast_opt/utilities/Datatype.h
+        ${ast_optimizer_SOURCE_DIR}/include/ast_opt/utilities/NodeUtils.h
         ${ast_optimizer_SOURCE_DIR}/include/ast_opt/utilities/IVisitor.h
         ${ast_optimizer_SOURCE_DIR}/include/ast_opt/utilities/Operator.h
         ${ast_optimizer_SOURCE_DIR}/include/ast_opt/utilities/PlainVisitor.h
@@ -206,6 +220,7 @@ target_include_directories(ast_opt PUBLIC
         $<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>
         $<INSTALL_INTERFACE:include>)
 set_target_properties(ast_opt PROPERTIES PUBLIC_HEADER "${PUBLIC_HEADERS}")
+set_target_properties(ast_opt PROPERTIES POSITION_INDEPENDENT_CODE TRUE)
 target_compile_features(ast_opt PUBLIC cxx_std_17)
 
 if (SEAL_FOUND)
@@ -251,6 +266,13 @@ install(EXPORT ast_optTargets
         NAMESPACE ast_opt::
         DESTINATION lib/cmake/ast_opt)
 
+# Install pyabc package (for dev)
+find_package (Python3 COMPONENTS Interpreter Development)
+
+if(NOT Python3_Interpreter_FOUND)
+    message(WARNING "The ABC python frontend requires Python 3, but no installation
+        was found. Skipping frontend building...")
+endif()
 
 ##############################
 # TARGET: ast_demo
@@ -273,3 +295,28 @@ if (CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME)
     enable_testing()
     add_subdirectory(test EXCLUDE_FROM_ALL)
 endif ()
+
+
+####################################################################################
+# Python Frontend
+#
+# Use pybind11 as C++ wrapper for the Python frontend
+####################################################################################
+
+if(PYTHON_FRONTEND_BUILD)
+    include(FetchContent)
+    set(JSON_BuildTests OFF CACHE INTERNAL "")
+    set(FETCHCONTENT_UPDATES_DISCONNECTED ON)
+    FetchContent_Declare(
+            pybind11
+            GIT_REPOSITORY https://github.com/pybind/pybind11.git
+            GIT_TAG v2.7.1)
+
+    FetchContent_GetProperties(pybind11)
+    if(NOT pybind11_POPULATED)
+        FetchContent_Populate(pybind11)
+        add_subdirectory(${pybind11_SOURCE_DIR} ${pybind11_BINARY_DIR})
+    endif()
+    add_subdirectory(python)
+endif()
+
diff --git a/INSTALL.md b/INSTALL.md
new file mode 100644
index 00000000..e38ffff6
--- /dev/null
+++ b/INSTALL.md
@@ -0,0 +1,108 @@
+# ABC Installation Notes
+
+The following notes are mainly for an installation on Fedora or Debian.
+
+## Prerequisites
+
+### Packages
+
+Install `cmake`, `doxygen`, and `clang`/`gcc`.
+`libboost` and `python3` dev tools are needed for pybind11.
+
+Fedora:
+```
+dnf install cmake doxygen clang boost-devel python3-devel
+```
+
+Debian:
+```
+apt install cmake doxygen clang libboost-all-dev python3-dev
+```
+
+### Install SEAL
+
+#### Clone repo
+
+Currently, the most recent version of SEAL with which this project was tested is `3.6.6`.
+
+```
+git clone -b v3.6.6 https://github.com/microsoft/SEAL.git
+```
+
+#### Building
+
+Use the following commands to build SEAL. For more build options, see the official [SEAL repo](https://github.com/Microsoft/SEAL#getting-started).
+
+<!-- 
+TODO [Miro]
+@Alex: the `-DSEAL_THROW_ON_TRANSPARENT_CIPHERTEXT=OFF` is here because EVA requires it. Do we need it too?
+-->
+
+Enter the repo you just cloned (`cd SEAL` if you haven't changed the name). Then build it as follows:
+```
+cmake -DSEAL_THROW_ON_TRANSPARENT_CIPHERTEXT=OFF -S . -B build
+cmake --build build
+```
+
+#### Install
+
+Install the library.
+
+```
+sudo cmake --install build   
+```
+
+## Install ABC
+
+### Clone repo
+
+```
+git clone git@github.com:MarbleHE/ABC.git
+```
+
+### Use CMakefiles
+
+The project is developed with CLion. Open it there and let the CMakefiles install all dependencies.
+
+#### Manually
+Run the following commands to build:
+```
+cmake -S . -B build
+cmake --build build
+```
+
+Install it:
+```
+sudo cmake --install build 
+```
+
+## Install pyabc Python Frontend
+
+If you want to use the python frontend, you can install our package `pyabc`.
+
+### Over CMake
+The package installation is integrated in the CMakefile. You can run `make install` in `cmake-build-debug` and it will install `pyabc`.
+
+### Manual Installation
+To install `pyabc`, first build the CMake project. Next, run the following command (in this repo's root folder):
+```
+python3 -m pip install --user cmake-build-debug/python
+```
+(assuming your build folder is `cmake-build-debug`)
+
+For a developer installation, add the `-e` option to create a symlink to the freshly built files in `cmake-build-debug/python` instead of copying them.
+
+## Check implementation
+
+1. Check that the CMake project runs through without any fatal error 
+
+    - Troubleshooting: first, try to use "Reload Cmake Project" and/or delete the `cmake-build-debug` folder to make a fresh new build.
+2. Run the "testing-all" target in CLion to execute all tests and make sure they pass on your local system. Some tests are disabled.
+    - Troubleshooting: if this entry is missing, do the following to add it:
+      - Open the dropdown menu with "Run/Debug Configurations"
+      - Select "Edit Configurations"
+      - Go to Google Tests
+      - Click "add new run configuration"
+      - Name it "testing-all"
+      - Select "resting-all" as target
+      - Save it and run (play symbol) the target
diff --git a/README.md b/README.md
index 3f14bab1..e95491cc 100644
--- a/README.md
+++ b/README.md
@@ -107,7 +107,7 @@ The implementation is heavily based on the pseudo-codes provided in the paper, e
 
 Before starting, make sure to clone this repository using: 
 ```
-git clone https://github.com/pjattke/msc-thesis-code-AST.git
+git clone https://github.com/MarbleHE/ABC.git
 ```
 
 The following tools are required to get this project running:
@@ -120,6 +120,8 @@ The following tools are required to get this project running:
 
 The easiest way to use this library is to import the project into [CLion](https://www.jetbrains.com/clion/) which automatically loads the containing cmake build files and adds the respective targets. Development was carried out on macO (10.15.2), although the project should be running on Windows or Linux machines too.
 
+More detailed installation notes, including some for Debian/Ubuntu and Fedora are in [INSTALL.md](INSTALL.md).
+
 The entire framework is built around ASTs,
 the nodes of which are implemented by a class hierarchy that derives from AbstractNode (see above).
 The AST nodes "own" (std::unique_ptr) their children (and, transitively, the entire subgraph).
diff --git a/include/ast_opt/ast/Assignment.h b/include/ast_opt/ast/Assignment.h
index fd03a430..5d5babff 100644
--- a/include/ast_opt/ast/Assignment.h
+++ b/include/ast_opt/ast/Assignment.h
@@ -100,6 +100,10 @@ class Assignment : public AbstractStatement {
   /// \param newValue new value to set
   void setValue(std::unique_ptr<AbstractExpression> newValue);
 
+  /// Create an Assignment node from a nlohmann::json representation of this node.
+  /// \return unique_ptr to a new Assignment node
+  static std::unique_ptr<Assignment> fromJson(nlohmann::json j);
+
   ///////////////////////////////////////////////
   ////////// AbstractNode Interface /////////////
   ///////////////////////////////////////////////
@@ -111,6 +115,7 @@ class Assignment : public AbstractStatement {
   size_t countChildren() const override;
   nlohmann::json toJson() const override;
   std::string toString(bool printChildren) const override;
+
  protected:
   std::string getNodeType() const override;
 };
diff --git a/include/ast_opt/ast/Ast.h b/include/ast_opt/ast/Ast.h
new file mode 100644
index 00000000..1a52c9e8
--- /dev/null
+++ b/include/ast_opt/ast/Ast.h
@@ -0,0 +1,26 @@
+#ifndef AST_OPTIMIZER_INCLUDE_AST_OPT_AST_AST_H_
+#define AST_OPTIMIZER_INCLUDE_AST_OPT_AST_AST_H_
+
+#include "AbstractExpression.h"
+#include "AbstractNode.h"
+#include "AbstractStatement.h"
+#include "AbstractTarget.h"
+#include "Assignment.h"
+#include "BinaryExpression.h"
+#include "Block.h"
+#include "Call.h"
+#include "ExpressionList.h"
+#include "For.h"
+#include "Function.h"
+#include "FunctionParameter.h"
+#include "If.h"
+#include "IndexAccess.h"
+#include "Literal.h"
+#include "OperatorExpression.h"
+#include "Return.h"
+#include "TernaryOperator.h"
+#include "UnaryExpression.h"
+#include "Variable.h"
+#include "VariableDeclaration.h"
+
+#endif //AST_OPTIMIZER_INCLUDE_AST_OPT_AST_AST_H_
diff --git a/include/ast_opt/ast/BinaryExpression.h b/include/ast_opt/ast/BinaryExpression.h
index b39bd204..314223dc 100644
--- a/include/ast_opt/ast/BinaryExpression.h
+++ b/include/ast_opt/ast/BinaryExpression.h
@@ -107,6 +107,10 @@ class BinaryExpression : public AbstractExpression {
   /// \param newRight new right hand side to set
   void setRight(std::unique_ptr<AbstractExpression> newRight);
 
+  /// Create a BinaryExpression node from a nlohmann::json representation of this node.
+  /// \return unique_ptr to a new BinaryExpression node
+  static std::unique_ptr<BinaryExpression> fromJson(nlohmann::json j);
+
   ///////////////////////////////////////////////
   ////////// AbstractNode Interface /////////////
   ///////////////////////////////////////////////
diff --git a/include/ast_opt/ast/Block.h b/include/ast_opt/ast/Block.h
index 13c3e492..1eb9918a 100644
--- a/include/ast_opt/ast/Block.h
+++ b/include/ast_opt/ast/Block.h
@@ -91,6 +91,10 @@ class Block : public AbstractStatement {
   /// Removes any potential nullptrs from the statements vector
   void removeNullStatements();
 
+  /// Create a Block node from a nlohmann::json representation of this node.
+  /// \return unique_ptr to a new Block node
+  static std::unique_ptr<Block> fromJson(nlohmann::json j);
+
   ///////////////////////////////////////////////
   ////////// AbstractNode Interface /////////////
   ///////////////////////////////////////////////
diff --git a/include/ast_opt/ast/ExpressionList.h b/include/ast_opt/ast/ExpressionList.h
index b6acbfe6..b77ef842 100644
--- a/include/ast_opt/ast/ExpressionList.h
+++ b/include/ast_opt/ast/ExpressionList.h
@@ -93,6 +93,10 @@ class ExpressionList : public AbstractExpression {
   /// Removes any potential nullptrs from the expressions vector
   void removeNullExpressions();
 
+  /// Create an ExpressionList node from a nlohmann::json representation of this node.
+  /// \return unique_ptr to a new ExpressionList node
+  static std::unique_ptr<ExpressionList> fromJson(nlohmann::json j);
+
   ///////////////////////////////////////////////
   ////////// AbstractNode Interface /////////////
   ///////////////////////////////////////////////
diff --git a/include/ast_opt/ast/For.h b/include/ast_opt/ast/For.h
index 78413478..edbc7335 100644
--- a/include/ast_opt/ast/For.h
+++ b/include/ast_opt/ast/For.h
@@ -133,6 +133,10 @@ class For : public AbstractStatement {
   /// \param newBody body to set, For takes ownership
   void setBody(std::unique_ptr<Block> newBody);
 
+  /// Create a For node from a nlohmann::json representation of this node.
+  /// \return unique_ptr to a new For node
+  static std::unique_ptr<For> fromJson(nlohmann::json j);
+
   ///////////////////////////////////////////////
   ////////// AbstractNode Interface /////////////
   ///////////////////////////////////////////////
diff --git a/include/ast_opt/ast/IndexAccess.h b/include/ast_opt/ast/IndexAccess.h
index 905cbe31..8c788516 100644
--- a/include/ast_opt/ast/IndexAccess.h
+++ b/include/ast_opt/ast/IndexAccess.h
@@ -94,6 +94,9 @@ class IndexAccess : public AbstractTarget {
   /// \param newIndex condition to set, IndexAccess takes ownership
   void setIndex(std::unique_ptr<AbstractExpression> &&newIndex);
 
+  /// Create a Block node from a nlohmann::json representation of this node.
+  /// \return unique_ptr to a new Block node
+  static std::unique_ptr<IndexAccess> fromJson(nlohmann::json j);
 
   ///////////////////////////////////////////////
   ////////// AbstractNode Interface /////////////
diff --git a/include/ast_opt/ast/Literal.h b/include/ast_opt/ast/Literal.h
index 2c89df08..5eff869c 100644
--- a/include/ast_opt/ast/Literal.h
+++ b/include/ast_opt/ast/Literal.h
@@ -118,6 +118,10 @@ class Literal : public AbstractExpression {
 #include "ast_opt/utilities/warning_hidingNonVirtualFunction_epilogue.h"
 
 
+  static std::unique_ptr<Literal<T>> fromJson(nlohmann::json j) {
+    return std::make_unique<Literal<T>>(j["value"].get<T>());
+  }
+
 
   ///////////////////////////////////////////////
   ////////// AbstractNode Interface /////////////
diff --git a/include/ast_opt/ast/Return.h b/include/ast_opt/ast/Return.h
index dd4c8268..44cb6f1a 100644
--- a/include/ast_opt/ast/Return.h
+++ b/include/ast_opt/ast/Return.h
@@ -68,6 +68,10 @@ class Return : public AbstractStatement {
   /// \param newValue new value to set
   void setValue(std::unique_ptr<AbstractExpression> newValue);
 
+  /// Create a Return node from a nlohmann::json representation of this node.
+  /// \return unique_ptr to a new Return node
+  static std::unique_ptr<Return> fromJson(nlohmann::json j);
+
   ///////////////////////////////////////////////
   ////////// AbstractNode Interface /////////////
   ///////////////////////////////////////////////
diff --git a/include/ast_opt/ast/Variable.h b/include/ast_opt/ast/Variable.h
index 8d047ff1..52c5f16a 100644
--- a/include/ast_opt/ast/Variable.h
+++ b/include/ast_opt/ast/Variable.h
@@ -49,6 +49,10 @@ class Variable : public AbstractTarget {
 
   [[nodiscard]] std::string getIdentifier() const;
 
+  /// Create a Variable node from a nlohmann::json representation of this node.
+  /// \return unique_ptr to a new Variable node
+  static std::unique_ptr<Variable> fromJson(nlohmann::json j);
+
   ///////////////////////////////////////////////
   ////////// AbstractNode Interface /////////////
   ///////////////////////////////////////////////
diff --git a/include/ast_opt/ast/VariableDeclaration.h b/include/ast_opt/ast/VariableDeclaration.h
index 712bffb8..3037e70b 100644
--- a/include/ast_opt/ast/VariableDeclaration.h
+++ b/include/ast_opt/ast/VariableDeclaration.h
@@ -121,6 +121,10 @@ class VariableDeclaration : public AbstractStatement {
   /// \param newValue new value to set
   void setValue(std::unique_ptr<AbstractExpression> newValue);
 
+  /// Create a VariableDeclaration node from a nlohmann::json representation of this node.
+  /// \return unique_ptr to a new VariableDeclaration node
+  static std::unique_ptr<VariableDeclaration> fromJson(nlohmann::json j);
+
   ///////////////////////////////////////////////
   ////////// AbstractNode Interface /////////////
   ///////////////////////////////////////////////
diff --git a/include/ast_opt/compiler/Compiler.h b/include/ast_opt/compiler/Compiler.h
new file mode 100644
index 00000000..c96822b8
--- /dev/null
+++ b/include/ast_opt/compiler/Compiler.h
@@ -0,0 +1,59 @@
+#ifndef AST_OPTIMIZER_INCLUDE_AST_OPT_COMPILER_COMPILER_H_
+#define AST_OPTIMIZER_INCLUDE_AST_OPT_COMPILER_COMPILER_H_
+
+#include "ast_opt/ast/AbstractNode.h"
+#include "ast_opt/runtime/RuntimeVisitor.h"
+
+/// The compiler parses a program (given in JSON or pseudo-c++) and executes it.
+class Compiler {
+ private:
+  /// Execute a program given as AST using the inputs provided as a block of variable declarations and returning
+  /// OutputIdentifierValuePairs for the outputs specified as AST.
+  /// \param programAst: program AST
+  /// \param inputBlock: Block of statements, each declaring one input variable
+  /// \param outputAst: output AST, must be a block of assignment statements, where on the RHS we only have
+  ///                   variables or index access statements.
+  /// \return: OutputIdentifierValuePairs for the identifiers specified in outputAst
+  static OutputIdentifierValuePairs compileAst(std::unique_ptr<AbstractNode> programAst,
+                                               std::unique_ptr<Block> inputBlock,
+                                               std::unique_ptr<AbstractNode> outputAst);
+
+  /// Transform a vector of identifiers to a block of (variable-to-variable) assignements.
+  /// \param outputIdentifiers: vector of identifiers
+  /// \return: an ABC AST block with a v = v; statement for every variable v in outputIdentifiers
+  static std::unique_ptr<AbstractNode> buildOutputBlock(std::vector<std::string> outputIdentifiers);
+
+ public:
+
+  /// Execute a program given as C++ pseudo-code using the inputs provided as c++ pseudo-code and returning
+  /// OutputIdentifierValuePairs for the outputs specified by identifiers given as vector.
+  /// \param program: C++ pseudo-code program
+  /// \param input: C++ pseudo-code input AST
+  /// \param outputIdentifiers: vector of identifiers of the variables (in the input program) that should be returned.
+  /// \return: OutputIdentifierValuePairs for the identifiers specified in outputIdentifiers
+  static OutputIdentifierValuePairs compile(std::string program,
+                                            std::string input,
+                                            std::vector<std::string> outputIdentifiers);
+
+
+  /// Execute a program given as JSON ABC AST using the inputs provided as JSON ABC AST and returning
+  /// OutputIdentifierValuePairs for the outputs specified by identifiers given as vector.
+  /// \param program: JSON ABC AST pseudo-code program
+  /// \param input: JSON ABC AST pseudo-code input AST
+  /// \param outputIdentifiers: vector of identifiers of the variables (in the input program) that should be returned.
+  /// \return: OutputIdentifierValuePairs for the identifiers specified in outputIdentifiers
+  static OutputIdentifierValuePairs compileJson(std::string program,
+                                                std::string input,
+                                                std::vector<std::string> outputIdentifiers);
+
+  /// Execute a program given as ABC AST using the inputs provided as JSON ABC AST and returning
+  /// OutputIdentifierValuePairs for the outputs specified by identifiers given as vector.
+  /// \param program: ABC AST pseudo-code program
+  /// \param input: JSON ABC AST pseudo-code input AST
+  /// \param outputIdentifiers: vector of identifiers of the variables (in the input program) that should be returned.
+  /// \return: OutputIdentifierValuePairs for the identifiers specified in outputIdentifiers
+  static OutputIdentifierValuePairs compileJson(std::unique_ptr<AbstractNode> programAst,
+                                                std::string input,
+                                                std::vector<std::string> outputIdentifiers);
+};
+#endif //AST_OPTIMIZER_INCLUDE_AST_OPT_COMPILER_COMPILER_H_
diff --git a/include/ast_opt/parser/Parser.h b/include/ast_opt/parser/Parser.h
index 120fc9eb..4ef3c612 100644
--- a/include/ast_opt/parser/Parser.h
+++ b/include/ast_opt/parser/Parser.h
@@ -94,5 +94,22 @@ class Parser {
   /// \return (A unique pointer) to the root node of the AST.
   static std::unique_ptr<AbstractNode> parse(std::string s,
                                              std::vector<std::reference_wrapper<AbstractNode>> &createdNodesList);
+
+  /// Parses the JSON string representation of an AST and returns (a unique ptr) to the created root node of the AST.
+  /// \param s The JSON string to parse
+  /// \return (A unique pointer) to the root node of the AST.
+  static std::unique_ptr<AbstractNode> parseJson(std::string s);
+
+  /// Parses the JSON representation of an AST and returns (a unique ptr) to the created root node of the AST.
+  /// \param j The JSON structure to parse
+  /// \throws stork::runtime_error if an unknown type is encountered
+  /// \return (A unique pointer) to the root node of the AST.
+  static std::unique_ptr<AbstractNode> parseJson(nlohmann::json j);
+  /// The following three functions are helpers for parseJson, so that we can get the right abstract node type,
+  /// because otherwise we would need to cast down from an AbstractNode.
+  static std::unique_ptr<AbstractExpression> parseJsonExpression(nlohmann::json j);
+  static std::unique_ptr<AbstractStatement> parseJsonStatement(nlohmann::json j);
+  static std::unique_ptr<AbstractTarget> parseJsonTarget(nlohmann::json j);
+
 };
 #endif //AST_OPTIMIZER_INCLUDE_AST_OPT_PARSER_PARSER_H_
diff --git a/include/ast_opt/runtime/RuntimeVisitor.h b/include/ast_opt/runtime/RuntimeVisitor.h
index 5bce7de5..472d81f1 100644
--- a/include/ast_opt/runtime/RuntimeVisitor.h
+++ b/include/ast_opt/runtime/RuntimeVisitor.h
@@ -2,6 +2,7 @@
 #define GRAPHNODE_H_INCLUDE_AST_OPT_VISITOR_RUNTIME_H_
 
 #include <stack>
+#include <string>
 
 #include "Cleartext.h"
 #include "ast_opt/utilities/Visitor.h"
@@ -50,6 +51,9 @@ class SpecialRuntimeVisitor : public ScopedVisitor {
   /// of its dependant nodes (e.g., operand of binary expression) are secret.
   SecretTaintedNodesMap &secretTaintedMap;
 
+  /// Check if a variable is secret. We assume it is not secret if it is not in the secret taint map.
+  bool isVariableSecretTainted(const std::string uniqueNodeId);
+
  public:
   /// Create a new SpecialRuntimeVisitor.
   /// \param factory A reference to the factory that should be used to create the ciphertexts.
diff --git a/include/ast_opt/utilities/Datatype.h b/include/ast_opt/utilities/Datatype.h
index 24efb87a..e5ca16db 100644
--- a/include/ast_opt/utilities/Datatype.h
+++ b/include/ast_opt/utilities/Datatype.h
@@ -9,9 +9,14 @@ enum class Type {
   BOOL, CHAR, INT, FLOAT, DOUBLE, STRING, VOID
 };
 
+const Type all_types[] = {Type::BOOL, Type::CHAR, Type::INT, Type::FLOAT, Type::DOUBLE, Type:: STRING, Type::VOID};
+
 /// String representation of enums
 std::string enumToString(const Type type);
 
+/// Enum from string representation
+Type stringToTypeEnum(const std::string s);
+
 /// A Datatype consists of a Type and a secret? flag
 class Datatype {
  private:
@@ -21,6 +26,9 @@ class Datatype {
   /// If this is a secret/encrypted version
   bool isSecret;
 
+  /// Prefix to mark secret types
+  static const std::string secretPrefix;
+
  public:
 
   /// Create a Datatype with underlying type and secret-ness
@@ -28,6 +36,10 @@ class Datatype {
   /// \param isSecret (optional) is this a secret/encrypted type?
   explicit Datatype(Type type, bool isSecret = false);
 
+  /// Create a Datatype with underlying type and secret-ness from a string (as stored in JSON)
+  /// \param typeString Type as string with the prefix "secret" for secret values
+  explicit Datatype(std::string typeString);
+
   /// Two types are equal iff they have the same underlying type and isSecret flag
   /// \param rhs Type to compare against
   /// \return true iff underlying type and isSecret flag agree
diff --git a/include/ast_opt/utilities/NodeUtils.h b/include/ast_opt/utilities/NodeUtils.h
new file mode 100644
index 00000000..7eca87b2
--- /dev/null
+++ b/include/ast_opt/utilities/NodeUtils.h
@@ -0,0 +1,57 @@
+#ifndef AST_OPTIMIZER_INCLUDE_AST_OPT_UTILITIES_NODEUTILS_H_
+#define AST_OPTIMIZER_INCLUDE_AST_OPT_UTILITIES_NODEUTILS_H_
+
+#include <string>
+#include <memory>
+#include <stdexcept>
+#include <ast_opt/parser/Errors.h>
+
+enum NodeType : unsigned char {
+  // AbstractStatement
+  NodeTypeAssignment = 0, NodeTypeBlock, NodeTypeFor, NodeTypeIf, NodeTypeReturn, NodeTypeVariableDeclaration,
+
+  // AbstractExpression -> AbstractTarget
+  NodeTypeFunctionParameter, NodeTypeIndexAccess, NodeTypeVariable,
+
+  // AbstractExpression
+  NodeTypeBinaryExpression, NodeTypeOperatorExpression, NodeTypeUnaryExpression, NodeTypeCall,
+  NodeTypeExpressionList, NodeTypeLiteralBool, NodeTypeLiteralChar, NodeTypeLiteralInt, NodeTypeLiteralFloat,
+  NodeTypeLiteralDouble, NodeTypeLiteralString, NodeTypeTernaryOperator,
+
+  // Symbol to mark the end of enum types and not actually a node type
+  NodeTypeLastSymbol
+};
+
+
+/// Cast a unique_ptr of (node) type S to (node) subtype T
+// TODO: change RuntimeVisitor to also use this function, once it's merged.
+template<typename S, typename T>
+std::unique_ptr<T> castUniquePtr(std::unique_ptr<S> &&source) {
+  if (dynamic_cast<T *>(source.get())) {
+    return std::unique_ptr<T>(dynamic_cast<T *>(source.release()));
+  } else {
+    throw stork::runtime_error("castUniquePtr failed: Cannot cast given unique_ptr from type "
+                                 + std::string(typeid(S).name()) + " to type " + std::string(typeid(T).name()) + ".");
+  }
+}
+
+class NodeUtils {
+ public:
+
+  /// Convert a NodeType enum to string
+  static std::string enumToString(const NodeType type);
+
+  /// Convert string to enum, returns the enum or LastSymbol if no enum with this name was found.
+  static NodeType stringToEnum(const std::string s);
+
+  /// Return true when the given string represents a subclass of an AbstractStatement node
+  static bool isAbstractStatement(const std::string s);
+
+  /// Return true when the given string represents a subclass of an AbstractTarget node
+  static bool isAbstractTarget(const std::string s);
+
+  /// Return true when the given string represents a subclass of an AbstractExpression node
+  static bool isAbstractExpression(const std::string s);
+};
+
+#endif //AST_OPTIMIZER_INCLUDE_AST_OPT_UTILITIES_NODEUTILS_H_
diff --git a/include/ast_opt/utilities/Operator.h b/include/ast_opt/utilities/Operator.h
index e4f918f8..f8979d33 100644
--- a/include/ast_opt/utilities/Operator.h
+++ b/include/ast_opt/utilities/Operator.h
@@ -28,6 +28,7 @@ enum UnaryOp : unsigned char {
 };
 
 // generate a typedef for this std::variant to ensure that always the same Enums order is used
+// Keep opStringArrays in Operator.cpp synchronized with OperatorVariant.
 typedef std::variant<ArithmeticOp, LogicalOp, UnaryOp> OperatorVariant;
 
 std::string toString(OperatorVariant opVar);
@@ -38,6 +39,11 @@ std::string toString(LogicalOp logop);
 
 std::string toString(UnaryOp uop);
 
+// Forward declaration
+class Operator;
+
+// Translate string (generated with toString from Operation) back to an Operation.
+Operator fromStringToOperatorVariant(std::string opString);
 
 class Operator {
  private:
@@ -46,6 +52,10 @@ class Operator {
   friend int comparePrecedence(const Operator &op1, const Operator &op2);
 
  public:
+  static inline const std::string binaryOpStrings[] = {"+", "-", "*", "/", "%", "+++", "---", "***"};
+  static inline const std::string logicalOpStrings[] = {"&&", "||", "<", "<=", ">", ">=", "==", "!=", "&", "^", "|"};
+  static inline const std::string unaryOpStrings[] = {"!", "~"};
+
   explicit Operator(OperatorVariant op);
 
   [[nodiscard]] bool isRightAssociative() const;
diff --git a/python/.gitignore b/python/.gitignore
new file mode 100644
index 00000000..8238c057
--- /dev/null
+++ b/python/.gitignore
@@ -0,0 +1,3 @@
+__pycache__/
+dist/
+cmake-build-debug/
diff --git a/python/CMakeLists.txt b/python/CMakeLists.txt
new file mode 100644
index 00000000..f3d427af
--- /dev/null
+++ b/python/CMakeLists.txt
@@ -0,0 +1,15 @@
+
+configure_file(setup.py.in setup.py.after_configure)
+file(GENERATE OUTPUT setup.py INPUT ${CMAKE_CURRENT_BINARY_DIR}/setup.py.after_configure)
+
+add_subdirectory(pyabc)
+
+add_custom_target(python ALL DEPENDS pyabc)
+add_custom_command(TARGET python
+    POST_BUILD
+    COMMAND ${CMAKE_COMMAND} -E copy_directory
+    ${CMAKE_CURRENT_SOURCE_DIR}/pyabc
+    ${CMAKE_CURRENT_BINARY_DIR}/pyabc
+)
+
+install(CODE "execute_process(COMMAND ${Python3_EXECUTABLE} -m pip install ${CMAKE_BINARY_DIR}/python)")
diff --git a/python/pyabc/ABCContext.py b/python/pyabc/ABCContext.py
new file mode 100644
index 00000000..31290566
--- /dev/null
+++ b/python/pyabc/ABCContext.py
@@ -0,0 +1,42 @@
+
+import logging
+
+from ast import *
+from inspect import getsource
+from sys import _getframe
+
+from .ABCVisitor import ABCVisitor
+from .ABCProgram import ABCProgram
+
+class ABCContext():
+    """
+    The context manager is used to mark the scope where we parse Python code and
+    build the ABC AST.
+    """
+
+    def __init__(self, prog : ABCProgram, log_level=logging.INFO):
+        self.log_level = log_level
+        logging.basicConfig(level=self.log_level)
+        self.prog = prog
+
+    def __enter__(self):
+        # Get the source code of the parent frame
+        parent_frame = _getframe(1)
+        src = getsource(parent_frame)
+        python_ast = parse(src)
+
+        # Store src code and AST in program. The src file path is used to get the entire src code from the calling file
+        # (and not only the functions that are on the call stack to this function call).
+        self.prog.set_src(parent_frame)
+
+        # Find the current 'with' block in the source code
+        for item in walk(python_ast):
+            if isinstance(item, With) and item.lineno == parent_frame.f_lineno - parent_frame.f_code.co_firstlineno + 1:
+                logging.debug(f"Start parsing With block at line {item.lineno}")
+
+                for block in item.body:
+                    ABCVisitor(self.prog, self.log_level).visit(block)
+                break
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        pass
\ No newline at end of file
diff --git a/python/pyabc/ABCGenericExpr.py b/python/pyabc/ABCGenericExpr.py
new file mode 100644
index 00000000..c217b068
--- /dev/null
+++ b/python/pyabc/ABCGenericExpr.py
@@ -0,0 +1,22 @@
+
+import logging
+
+from .ABCJsonAstBuilder import ABCJsonAstBuilder
+
+class ABCGenericExpr():
+    def __init__(self, expr, log_level=logging.INFO):
+        self.expr = expr
+
+        self.log_level = log_level
+        logging.basicConfig(level=log_level)
+
+        self.builder = ABCJsonAstBuilder(log_level=log_level)
+
+    def __add__(self, other):
+        return ABCGenericExpr(
+            self.builder.make_binary_expression(self.expr, self.builder.constants.ADD, other.expr),
+            log_level=self.log_level
+        )
+
+    # TODO: error for unsupported expressions
+    # TODO: support more expressions
diff --git a/python/pyabc/ABCJsonAstBuilder.py b/python/pyabc/ABCJsonAstBuilder.py
new file mode 100644
index 00000000..42395956
--- /dev/null
+++ b/python/pyabc/ABCJsonAstBuilder.py
@@ -0,0 +1,315 @@
+
+import logging
+
+class ABCJsonAstBuilder:
+    """
+    Provide helper functions to create dictionary elements that correspond to ABC AST nodes when exported as JSON.
+    """
+
+    def __init__(self, log_level=logging.INFO):
+        self.log_level = log_level
+        logging.basicConfig(level=log_level)
+
+    #
+    # "Public" constants
+    #
+    class constants:
+        LT = "<"
+        GT = ">"
+        LTE = "<="
+        GTE = ">="
+        EQ = "="
+        AND = "&&"
+        OR = "||"
+
+        ADD = "+"
+        SUB = "-"
+        DIV = "/"
+        MOD = "%"
+        MUL = "*"
+
+        SECRET_PREFIX = "secret "
+
+    #
+    # Internal helper function to create attributes for ABC nodes
+    #
+    def _assert_single_type_list(self, l, t):
+        if any(map(lambda e: type(e) != t, l)):
+            logging.error(f"Lists of mixed types are not supported. Expected all elements to be of type {t}.")
+            exit(1)
+
+    def _find_datatype(self, d):
+        """
+        We currently simply parse the JSON-equivalent dictionary d and then use the data type of the first literal that
+        we find.
+
+        This assumes that no type casting is supported and variables never change their type.
+
+        :param d: JSON-equivalent dictionary of a value of which we want to find the data type.
+        :return: data type "bool", "string", "char", "int", or "void"
+        """
+
+        if "type" in d and d["type"].startswith("Literal"):
+            if d["type"] == "LiteralBool":
+                type_name = "bool"
+            elif d["type"] == "LiteralString":
+                if len(d["value"]) > 1:
+                    type_name = "string"
+                else:
+                    type_name = "char"
+            elif d["type"] == "LiteralFloat":
+                # Actually, in C++ we have floats and doubles. But we can't distinguish them, since their ranges overlap.
+                # Thus, we just take the larger double to not loose precision.
+                type_name = "double"
+                logging.warning("Using double for Python float.")
+            elif d["type"] == "LiteralInt":
+                type_name = "int"
+            else:
+                type_name = "void"
+
+            return type_name
+        else:
+            if isinstance(d, dict):
+                for v in d.values():
+                    type_name = self._find_datatype(v)
+                    if type_name != "void":
+                        return type_name
+            elif isinstance(d, list):
+                # By convention, an ExpressionList has the type of its elements. I.e., a list of LiteralInt is of type
+                # LiteralInt too.
+                return self._find_datatype(d[0])
+
+            return "void"
+
+    def _make_abc_node(self, type, content):
+        d = {"type": type}
+        d.update(content)
+        return d
+
+    def _make_body(self, body):
+        return {"body": body}
+
+    def _make_condition(self, condition):
+        return {"condition": condition}
+
+    def _make_datatype(self, val, is_secret : bool):
+        type_name = self._find_datatype(val)
+
+        # Secret types just have a prefix in the type string in JSON
+        if is_secret:
+            type_name = self.constants.SECRET_PREFIX + type_name
+
+        return {"datatype": type_name}
+
+    def _make_identifier(self, identifier):
+        return {"identifier": identifier}
+
+    def _make_index(self, index):
+        return {"index": index}
+
+    def _make_initializer(self, initializer):
+        return {"initializer": initializer}
+
+    def _make_left(self, left):
+        return {"left": left}
+
+    def _make_op(self, op):
+        return {"operator": op}
+
+    def _make_right(self, right):
+        return {"right": right}
+
+    def _make_stmts(self, stmts):
+        return {"statements": stmts}
+
+    def _make_expressions(self, stmts):
+        return {"expressions": stmts}
+
+    def _make_target(self, target):
+        return {"target": target}
+
+    def _make_update(self, update):
+        return {"update": update}
+
+    def _make_value(self, value):
+        return {"value": value}
+
+
+    #
+    # "Public" functions to create ABC nodes
+    #
+    def make_assignment(self, target : dict, value : dict) -> dict:
+        """
+        Create a dictionary corresponding to an ABC assignment when exported in JSON
+
+        :param target: target (as dict), to which the value is assigned
+        :param value: value (as dict) of ABC node to assign to target
+        :return: JSON-equivalent dictionary for an ABC assignment
+        """
+
+        d = self._make_target(target)
+        d.update(self._make_value(value))
+
+        return self._make_abc_node("Assignment", d)
+
+    def make_binary_expression(self, left : dict, op : str, right : dict) -> dict:
+        """
+        Create a dictionary corresponding to an ABC BinaryExpression when exported in JSON
+
+        :param left: Expression (as dict) on the left of the operator
+        :param op: Operator (as string)
+        :param right: Expression (as dict) on the right of the operator
+        :return: JSON-equivalent dictionary for an ABC BinaryExpression
+        """
+
+        d = self._make_left(left)
+        d.update(self._make_op(op))
+        d.update(self._make_right(right))
+
+        return self._make_abc_node("BinaryExpression", d)
+
+    def make_block(self, stmts : list) -> dict:
+        """
+        Create a dictionary corresponding to an ABC block containing the given statements
+
+        :param stmts: List of JSON-equivalent dictionary ABC statement nodes
+        :return: JSON-equivalent dictionary for an ABC block
+        """
+
+        return self._make_abc_node("Block", self._make_stmts(stmts))
+
+    def make_expression_list(self, exprs : list) -> dict:
+        """
+        Create a dictionary corresponding to an ABC ExpressionList node when exported in JSON
+
+        :param exprs: List of JSON-equivalent dictionaries of AbstractExpressions
+        :return: JSON-equivalent dictionary for an ExpressionList node
+        """
+
+        return self._make_abc_node("ExpressionList", self._make_expressions(exprs))
+
+    def make_for(self, initializer : dict, condition : dict, update : dict, body : dict) -> dict:
+        """
+        Create a dictionary corresponding to an ABC For node when exported in JSON
+
+        :param initializer: JSON-equivalent dictionary for an ABC Block which initializes the loop variable
+        :param condition: JSON-equivalent dictionary for an ABC BinaryExpression. We execute the body as long as this
+                          condition is true.
+        :param update: JSON-equivalent dictionary for an ABC Block that updates the loop variable
+        :param body: JSON-equivalent dictionary for an ABC Block of statements that are executed in the loop
+        :return: JSON-equivalent dictionary for an ABC For node
+        """
+
+        d = self._make_initializer(initializer)
+        d.update(self._make_condition(condition))
+        d.update(self._make_update(update))
+        d.update(self._make_body(body))
+
+        return self._make_abc_node("For", d)
+
+    def make_index_access(self, target : dict, index : dict):
+        """
+        Create a dictionary corresponding to an ABC IndexAccess node when exported in JSON
+
+        :param target: JSON-equivalent dictionary for an ABC AbstractTarget
+        :param index: JSON-equivalent dictionary for an ABC AbstractExpression
+        :return: JSON-equivalent dictionary for an ABC IndexAccess node
+        """
+
+        d = self._make_target(target)
+        d.update(self._make_index(index))
+        return self._make_abc_node("IndexAccess", d)
+
+    def make_literal(self, value) -> dict:
+        """
+        Create a dictionary corresponding to an ABC Literal* when exported in JSON, where * depends on the type of value.
+
+        All Python floats will be translated to LiteralDouble.
+        All Python strings of length <= 1 to LiteralChar.
+        For other behaviour, special functions should be implemented for that purpose.
+
+        :param value: value of the node (int, float, bool, char, string)
+        :return: JSON-equivalent dictionary for an ABC Literal*
+        """
+
+        if isinstance(value, int):
+            return self._make_abc_node("LiteralInt", self._make_value(value))
+        elif isinstance(value, float):
+            return self._make_abc_node("LiteralDouble", self._make_value(value))
+        elif isinstance(value, bool):
+            return self._make_abc_node("LiteralBool", self._make_value(value))
+        elif isinstance(value, str):
+            if len(value) > 1:
+                return self._make_abc_node("LiteralString", self._make_value(value))
+
+            return self._make_abc_node("LiteralChar", self._make_value(value))
+        else:
+            logging.error(f"Unsupported type '{type(value)}': only int, float, bool, and str have corresponding ABC Literals.")
+
+    def make_return(self, value : dict) -> dict:
+        """
+        Create a dictionary corresponding to an ABC Return when exported in JSON
+
+        :param value: ABC Ast node (as dictionary) to return
+        :return: JSON-equivalent dictionary for an ABC Return
+        """
+
+        return self._make_abc_node("Return", self._make_value(value))
+
+    def make_update(self, target : dict, op : str, value : dict) -> dict:
+        """
+        Create a dictionary corresponding to an update Block of a ABC For AST node.
+
+        :param target: JSON-equivalent dictionary of the target variable
+        :param op: arithmetic operation (represented as string, must be in self.constants)
+        :param value: JSON-equivalent dictionary of the value.
+        :return: JSON-equivalent of a block with the statement "target = `target` `op` `value`"
+        """
+
+        stmts = []
+
+        update_expr = self.make_binary_expression(target, op, value)
+        assignment_expr = self.make_assignment(target, update_expr)
+        stmts.append(assignment_expr)
+
+        return self.make_block(stmts)
+
+    def make_value(self, value) -> dict:
+        """
+        Helper function to either create a Literal* or an ExpressionList ABC node.
+
+        :param value: Basic python type (bool, str, int, float) or list (of such a basic type)
+        :return: JSON-equivalent dictionary for either a Literal* or an ExpressionList ABC node
+        """
+
+        if isinstance(value, list):
+            self._assert_single_type_list(value[1:], type(value[0]))
+            return self.make_expression_list(list(map(self.make_value, value)))
+        else:
+            return self.make_literal(value)
+
+    def make_variable(self, identifier : str) -> dict:
+        """
+        Create a dictionary corresponding to an ABC variable when exported in JSON
+
+        :param identifier: Identifier of the variable
+        :return: JSON-equivalent dictionary for an ABC variable
+        """
+
+        return self._make_abc_node("Variable", self._make_identifier(identifier))
+
+    def make_variable_declaration(self, target : dict, value : dict, is_secret : bool = False) -> dict:
+        """
+        Create a dictionary corresponding to an ABC variable declaration when exported in JSON
+
+        :param target: variable (as dict), to which the value is assigned
+        :param value: value (as dict) of ABC node to assign to target
+        :param is_secret: Boolean set to true if the given value is secret (default: False)
+        :return: JSON-equivalent dictionary for an ABC assignment
+        """
+
+        d = self._make_target(target)
+        d.update(self._make_value(value))
+        d.update(self._make_datatype(value, is_secret))
+
+        return self._make_abc_node("VariableDeclaration", d)
\ No newline at end of file
diff --git a/python/pyabc/ABCProgram.py b/python/pyabc/ABCProgram.py
new file mode 100644
index 00000000..700a6be4
--- /dev/null
+++ b/python/pyabc/ABCProgram.py
@@ -0,0 +1,164 @@
+
+import logging
+import json
+import os
+
+from inspect import getsource, getmodule
+from ast import parse
+
+from ._abc_wrapper import *
+from .ABCJsonAstBuilder import ABCJsonAstBuilder
+
+class ABCProgram:
+    """
+    Class for an ABC FHE program
+    """
+
+    def __init__(self, log_level=logging.INFO):
+        self.log_level = log_level
+        logging.basicConfig(level=self.log_level)
+
+        self.builder = ABCJsonAstBuilder(log_level=log_level)
+
+    #
+    # Internal helper functions
+    #
+    def _create_fhe_args_block(self, fhe_args : dict) -> dict:
+        """
+        Translate a dictionary of variable-value assignments to a block of ABC variable declaration statements.
+
+        :param fhe_args: Dictionary of the form { var_name : (value, is_secret), ... }
+        :return: JSON dictionary equivalent to an AST subtree for a block of variable declaration statements.
+        """
+        stmts = []
+        for fhe_arg_name, fhe_arg_val in fhe_args.items():
+            abc_var = self.builder.make_variable(fhe_arg_name)
+            abc_val = self.builder.make_value(fhe_arg_val[0])
+            abc_var_decl = self.builder.make_variable_declaration(abc_var, abc_val, fhe_arg_val[1])
+            stmts.append(abc_var_decl)
+
+        return self.builder.make_block(stmts)
+
+
+    #
+    # External functions
+    #
+    def add_main_fn(self, body : dict, args : dict, ret_vars : dict = {}, ret_constants : list = []) -> None:
+        """
+        Add a main function and its arguments to the program.
+
+        :param body: ABC AST as a JSON-equivalent dictionary
+        :param args: Argument dictionary of the format described in ABCVisitor._args_to_dict
+        :param ret_vars: A dictionary with variable names as keys and the position in the return vector as value
+        :param ret_constants: A list with constant return values. This must be at least as long to cover all gaps in ret_vars.
+                              Additional values will be appended to the return vector.
+        """
+
+        self.main_fn         = body
+        self.main_args       = args
+        self.ret_vars        = ret_vars
+        self.ret_constants   = ret_constants
+
+        self.compile()
+
+    def compile(self):
+        """
+        Compile the main function to an executable ABC AST, i.e., parse the JSON representation to an ABC AST
+        but not yet specify input/output values.
+
+        :return: void, the compiled main function is stored internally in self.cpp_program.
+        """
+        if self.main_fn:
+            self.cpp_program = ABCProgramWrapper(json.dumps(self.main_fn), json.dumps(self.main_args))
+        else:
+            logging.error("Set main function first, before trying to compile!")
+            exit(1)
+
+    def execute(self, *args, **kwargs):
+        """
+        Executes the main function with the given variable assignments.
+        Raises an error when not all required variables are defined.
+
+        :param kwargs: arguments for the main function. Default values are used for undefined optional variables.
+        :return: whatever the defined FHE main function returns.
+        """
+
+        logging.debug(f"Running main function with args: {self.main_args}")
+
+        if not self.cpp_program:
+            self.compile()
+
+        # Ensure all arguments are present
+        if len(args) > len(self.main_args):
+            logging.error(f"Too many arguments supplied for main (max. {len(self.main_args)} are allowed).")
+            exit(1)
+
+        fhe_args = dict()
+        main_args_keys = list(self.main_args.keys())
+        ## Add positional arguments with names in the order of those specified in self.main_args.
+        for i, arg in enumerate(args):
+            fhe_args[main_args_keys[i]] = (arg, self.main_args[main_args_keys[i]]["secret"])
+
+        ## Add keyword arguments (if they are actually arguments of main)
+        for arg_name, arg_val in kwargs.items():
+            if arg_name not in main_args_keys:
+                logging.error(f"Unknown argument '{arg_name}' for main.")
+            fhe_args[arg_name] = (arg_val, self.main_args[arg_name]["secret"])
+
+        ## Add remaining arguments of main that have default values
+        for arg_name in list(main_args_keys)[len(args):]:
+            if arg_name not in kwargs:
+                arg_meta = self.main_args[arg_name]
+                if not arg_meta["opt"]:
+                    logging.error(f"Mandatory argument '{arg_name}' is missing!")
+                    exit(1)
+                fhe_args[arg_name] = (arg_meta["value"], arg_meta["secret"])
+
+        ## Create FHE arguments: make block of assignment statements
+        fhe_args_block = self._create_fhe_args_block(fhe_args)
+
+        ret_var_names = list(self.ret_vars.keys())
+        result = self.cpp_program.execute(json.dumps(fhe_args_block), ret_var_names)
+
+        res_vec = []
+        res_val_idx = 0
+        for ret_vars_idx, ret_var in enumerate (ret_var_names):
+            # Add constants to fill gaps between variables
+            res_vec += self.ret_constants[res_val_idx : self.ret_vars[ret_var]]
+            res_val_idx = self.ret_vars[ret_var]
+
+            # Add result for variable
+
+            # TODO: This is a hack necessary due to python's dynamic (return) types.
+            #   For now, lists that only have a single value are always converted to that value only.
+            #   In the future, we maybe could leverage type hints to know what return value python actually expects.
+            val = result[ret_vars_idx]
+            if isinstance(val, list) and len(val) == 1:
+                val = val[0]
+            res_vec.append(val)
+
+        # Append remaining constants
+        res_vec += self.ret_constants[res_val_idx:]
+
+        return res_vec[0] if len(res_vec) == 1 else tuple(res_vec)
+
+    def set_src(self, parent_frame):
+        """
+        Stores the source information. The context source stores the code of the last ABCContext call, the src code
+        stores the source code of the entire file from which the ABCContext was called from.
+        """
+
+        self.src_module = getmodule(parent_frame)
+        self.src_context = getsource(parent_frame)
+        self.src_context_ast = parse(self.src_context)
+
+        self.src_file_path = os.path.realpath(self.src_module.__file__)
+        with open(self.src_file_path, "r") as src_fp:
+            self.src_code = src_fp.read()
+        self.src_code_ast = parse(self.src_code)
+
+    def to_cpp_string(self):
+        if not self.cpp_program:
+            logging.error("No CPP program available yet, call compile() first")
+            exit(1)
+        return self.cpp_program.to_cpp_string()
diff --git a/python/pyabc/ABCTypes.py b/python/pyabc/ABCTypes.py
new file mode 100644
index 00000000..02f954b6
--- /dev/null
+++ b/python/pyabc/ABCTypes.py
@@ -0,0 +1,59 @@
+from typing import TypeVar, NewType, List
+
+# Generic types. Those rely on Python not enforcing types.
+Secret = TypeVar("Secret")
+NonSecret = TypeVar("NonSecret")
+
+# Specific types. Those are useful if we want to hint to the translation what value the arguments haves
+SecretInt = NewType("SecretInt", int)
+SecretFloat = NewType("SecretFloat", float)
+SecretDouble = NewType("SecretDouble", float)
+SecretBool = NewType("SecretBool", bool)
+SecretString = NewType("SecretString", str)
+SecretChar = NewType("SecretChar", str)
+
+SecretIntVector = NewType("SecretIntVector", List[int])
+SecretFloatVector = NewType("SecretFloatVector", List[float])
+SecretDoubleVector = NewType("SecretDoubleVector", List[float])
+SecretBoolVector = NewType("SecretBoolVector", List[bool])
+SecretStringVector = NewType("SecretStringVector", List[str])
+SecretCharVector = NewType("SecretCharVector", List[str])
+
+NonSecretInt = NewType("NonSecretInt", int)
+NonSecretFloat = NewType("NonSecretFloat", float)
+NonSecretDouble = NewType("NonSecretDouble", float)
+NonSecretBool = NewType("NonSecretBool", bool)
+NonSecretString = NewType("NonSecretString", str)
+NonSecretChar = NewType("NonSecretChar", str)
+
+NonSecretIntVector = NewType("NonSecretIntVector", List[int])
+NonSecretFloatVector = NewType("NonSecretFloatVector", List[float])
+NonSecretDoubleVector = NewType("NonSecretDoubleVector", List[float])
+NonSecretBoolVector = NewType("NonSecretBoolVector", List[bool])
+NonSecretStringVector = NewType("NonSecretStringVector", List[str])
+NonSecretCharVector = NewType("NonSecretCharVector", List[str])
+
+
+def is_secret(val_type):
+    """
+    Return true when a type is secret. All unknown values default to being secret.
+
+    :param val_type: either a Secret* or NonSecret* type as type or String representation of the type.
+                 Any other types default to being secret too.
+    :return: false if it is a NonSecret* type (as type or string), true otherwise
+    """
+
+    non_secret_types = [NonSecret, NonSecretInt, NonSecretFloat, NonSecretDouble, NonSecretBool, NonSecretString,
+                        NonSecretChar, NonSecretIntVector,
+                        NonSecretFloatVector, NonSecretDoubleVector, NonSecretBoolVector, NonSecretStringVector,
+                        NonSecretCharVector]
+
+    # Case 1: type is given as string, check if it corresponds to the string version of a non_secret type
+    if isinstance(val_type, str):
+        return val_type not in map(lambda x: x.__name__, non_secret_types)
+    # Case 2: type is given as TypeVar or NewType, compare it directly to our non-secret types
+    elif isinstance(val_type, type(TypeVar)) or isinstance(val_type, type(NewType)):
+        return val_type not in non_secret_types
+    # Case 3: any other type just defaults to being secret
+    else:
+        return True
diff --git a/python/pyabc/ABCVisitor.py b/python/pyabc/ABCVisitor.py
new file mode 100644
index 00000000..ea80a191
--- /dev/null
+++ b/python/pyabc/ABCVisitor.py
@@ -0,0 +1,735 @@
+import logging
+import json
+
+from ast import *
+
+from .ABCGenericExpr import ABCGenericExpr
+from .ABCJsonAstBuilder import ABCJsonAstBuilder
+from .ABCTypes import is_secret
+from .FunctionVisitor import FunctionVisitor
+
+UNSUPPORTED_ATTRIBUTE_RESOLUTION = "Attribute resolution is not supported."
+UNSUPPORTED_FUNCTION = "Functions other than 'main' are not supported (violating function: '%s')."
+UNSUPPORTED_ONLY_ARGS = "Positional-only and keyword-only arguments are not supported."
+UNSUPPORTED_STATEMENT = "Unsupported statement: '%s' is not supported."
+UNSUPPORTED_SYNTAX_ERROR = "Unsupported syntax: %s."
+INVALID_PYTHON_SYNTAX = "Invalid python syntax: %s."
+UNSUPPORTED_MULTI_VALUE_RETURN = "Return statements with multiple values are not supported (violating line '%s')"
+NO_FLOOR_DIV = "There is no type casting, division of integers will always be integer division. " \
+               "Thus '/' and '//' are equivalent in ABC frontend code."
+UNSUPPORTED_LOCAL_FUNCTION_CALL = "Function calls in the same context object are not yet implemented."
+UNSUPPORTED_GLOBAL_FUNCTION_CALL = "Global functions calls are not yet implemented."
+BLACKBOX_FUNCTION_CALL = "Did not find source code of function call, treat it as blackbox."
+
+MAIN_SYMBOL = "main"
+
+
+class ABCVisitor(NodeVisitor):
+    """
+    Visitor for the Python AST, which constructs a JSON file for an ABC AST.
+    """
+
+    def __init__(self, prog, log_level=logging.INFO, *args, **kwargs):
+        self.prog = prog
+
+        self.log_level = log_level
+        logging.basicConfig(level=log_level)
+
+        self.builder = ABCJsonAstBuilder(log_level=log_level)
+
+        # A set of variables that were already declared in the current subtree.
+        # This is necessary to decide between creating an Assignment node or a VariableDeclaration.
+        self.declared_vars = set()
+
+        super(ABCVisitor, self).__init__(*args, **kwargs)
+
+    #
+    # Helper functions
+    #
+    def _get_annotation(self, arg):
+        if hasattr(arg, "annotation") and arg.annotation:
+            return arg.annotation.id
+        else:
+            return None
+
+    def _tpl_stmt_to_list(self, t):
+        """
+        Convert a LHS or RHS, which may be a tuple of multiple values, to a list.
+
+        E.g., for `a, b = expr1, expr2` we have two tuples, one on the LHS with (a, b)
+        and one on the RHS with (expr1, expr2).
+
+        :param t: input, may be a tuple
+        :return: list of element(s) (unpacked tuple)
+        """
+        if isinstance(t, Tuple):
+            return list(t.elts)
+        return [t]
+
+    def _parse_lhs(self, lhs):
+        """
+        Parse a LHS to (supported) variables or throw an error for unsupported syntax.
+
+        :param lhs: LHS of an assignment
+        :return: a list of variables
+        """
+
+        # XXX: Currently, we do not support tuples in ABC, so we treat them differently at different
+        # places. For this reason, we have a helper function instead of using visit_Name, visit_Tuple directly.
+        if isinstance(lhs, Name) or isinstance(lhs, Subscript):
+            return [self.visit(lhs)]
+        elif isinstance(lhs, Tuple):
+            return list(map(self._parse_lhs, self._tpl_stmt_to_list(lhs)))
+        else:
+            logging.error(
+                UNSUPPORTED_SYNTAX_ERROR.format(f"type '{type(lhs)}' is not supported on the LHS of an assignment")
+            )
+            exit(1)
+
+    def _args_to_dict(self, args: arguments) -> dict:
+        """
+        Covert a Python function argument AST node to a dictionary of the following form:
+            {
+                var_name = {"opt": Bool, "value": value, "secret": Bool},
+                ...
+            }
+        where the item "opt" signals if the variable is optional and the second entry "value" stores
+        the default value for optional variables (and later the real value for non-optional ones).
+        "secret" stores the type hint information and is used to mark secret values.
+
+        :param args: Python argument AST node
+        :return: variable dictionary
+        """
+
+        def _make_arg_dict(secret, opt=False, val=None):
+            return {"opt": opt, "value": val, "secret": secret}
+
+        # Don't support positional only / kw only arguments
+        if len(args.kwonlyargs) + len(args.posonlyargs) > 0:
+            logging.error(UNSUPPORTED_ONLY_ARGS)
+            exit(1)
+
+        d = dict()
+        off = len(args.args) - len(args.defaults)
+
+        # Add arguments without default value
+        for i in range(off):
+            arg = args.args[i]
+            annotation = self._get_annotation(arg)
+            d[arg.arg] = _make_arg_dict(is_secret(annotation))
+
+        # Add arguments with default value
+        for i, default in enumerate(args.defaults):
+            # Arguments with a default can never be secret.
+            d[args.args[i + off].arg] = _make_arg_dict(False, True, default.value)
+
+        return d
+
+    def _make_assignment(self, var, expr):
+        if var["type"] == "Variable":
+            var_name = var["identifier"]
+            if var_name in self.declared_vars:
+                return self.builder.make_assignment(var, expr)
+            else:
+                self.declared_vars.add(var_name)
+                return self.builder.make_variable_declaration(var, expr)
+        else:
+            # we never declare an index assignment, the indexed variable was declared before.
+            return self.builder.make_assignment(var, expr)
+
+    #
+    # Supported visit functions
+    #
+
+    def visit_Add(self, node: Add) -> dict:
+        return self.builder.constants.ADD
+
+    def visit_And(self, node: And) -> dict:
+        return self.builder.constants.AND
+
+    def visit_Assign(self, node: Assign) -> dict:
+        """
+        Visit a Python assignment and transform it to a dictionary corresponding to one or more ABC assignments.
+        """
+
+        ## First, evaluate the RHS expression
+        exprs = list(map(self.visit, self._tpl_stmt_to_list(node.value)))
+
+        ## Second, create assignments by parsing the LHS targets
+        abc_assignments = []
+
+        ### targets can be a list of variables, e.g. ["a", "b"] for "a = b = expr" syntax
+        for target in node.targets:
+            vars = self._parse_lhs(target)
+
+            for var_idx, var in enumerate(vars):
+                expr = exprs[var_idx]
+                if isinstance(var, list):
+                    if not isinstance(expr, list) or len(var) != len(expr):
+                        logging.error(
+                            INVALID_PYTHON_SYNTAX.format("trying to unpack more RHS values than LHS variables")
+                        )
+                        exit(1)
+
+                    for i in range(len(var)):
+                        abc_assignments.append(
+                            self._make_assignment(var[i], expr[i])
+                        )
+                else:
+                    abc_assignments.append(
+                        self._make_assignment(var, expr)
+                    )
+
+        if len(abc_assignments) > 1:
+            # XXX: This could be supported with our current AST, but would require us to build the JSON string
+            # manually, so that one visit function can add more than one statement to the resulting string.
+            # Alternatively, support for this can be added to the AST.
+            logging.error(
+                UNSUPPORTED_SYNTAX_ERROR.format(f"Multi-assignments and pattern matching are not yet supported.")
+            )
+            exit(1)
+
+        return abc_assignments[0]
+
+    def visit_AugAssign(self, node: AugAssign) -> dict:
+        """
+        Visit a Python augmented assignment (e.g., +=, -=, *=, ...) and transform it to a dictionary corresponding to
+        an ABC assignments.
+        """
+
+        target = self.visit(node.target)
+        update = self.builder.make_binary_expression(target, self.visit(node.op), self.visit(node.value))
+        return self.builder.make_assignment(target, update)
+
+    def visit_BinOp(self, node: BinOp) -> dict:
+        return self.builder.make_binary_expression(
+            self.visit(node.left),
+            self.visit(node.op),
+            self.visit(node.right)
+        )
+
+    def visit_Call(self, node: Call) -> dict:
+        """
+        Handle function calls, depending on where the function is located:
+        - Internal functions (inside the same ABCContext) are not yet supported. In the future, they will be translated
+            to AST function calls.
+        - For external function calls we try to extract the expression created by them by running them at compile time
+            on generic FHE objects that track the operations performed on them.
+        """
+
+        # Check if the function is defined in the same context
+        if isinstance(node.func, Attribute):
+            logging.error(UNSUPPORTED_ATTRIBUTE_RESOLUTION)
+            exit(1)
+
+        fn_ast = FunctionVisitor(node.func.id).visit(self.prog.src_context_ast)
+        if fn_ast:
+            logging.error(UNSUPPORTED_LOCAL_FUNCTION_CALL)
+            exit(1)
+            # TODO: return here when this is implemented
+
+        # Check if the function is defined globally in the same file
+        fn_ast = FunctionVisitor(node.func.id).visit(self.prog.src_code_ast)
+        if fn_ast:
+            logging.error(UNSUPPORTED_GLOBAL_FUNCTION_CALL)
+            exit(1)
+            # TODO: return here when this is implemented
+
+        # Otherwise, treat this function as a blackbox call. We execute the function on generic ABC values and record
+        # the operations performed on those values to build an expression, with which we replace the function call.
+        logging.warning(BLACKBOX_FUNCTION_CALL)
+
+        # Create generic variables for all arguments. Turn normal arguments into AST variable nodes and recursively
+        # evaluate the value of keyword arguments.
+        args = [ABCGenericExpr(self.visit(arg)) for arg in node.args]
+        kwargs = {keyword.arg: ABCGenericExpr(self.visit(keyword.value)) for keyword in node.keywords}
+
+        # Get the blackbox function from the parent module and execute it on our tracing inputs.
+        external_fn_ret = getattr(self.prog.src_module, node.func.id)(*args, **kwargs)
+
+        # Return the AST expression gathered by the generic response object
+        return external_fn_ret.expr
+
+    def visit_Constant(self, node: Constant):
+        """
+        Visit a Python constant and transform it to a dictionary corresponding to an ABC literal.
+        """
+
+        return self.builder.make_literal(node.value)
+
+    def visit_Div(self, node: Div) -> dict:
+        return self.builder.constants.DIV
+
+    def visit_Eq(self, node: Eq) -> dict:
+        return self.builder.constants.EQ
+
+    def visit_FloorDiv(self, node: FloorDiv) -> dict:
+        logging.warning(NO_FLOOR_DIV);
+        return self.builder.constants.DIV
+
+    def visit_For(self, node: For) -> dict:
+        """
+        Visit a Python For node and convert it to an ABC AST For node.
+        """
+
+        target = self.visit(node.target)
+
+        # Currently only support iterating over range!
+        if node.iter.func.id != "range":
+            logging.error("The python frontend currently only supports iterating over a `range`")
+            exit(1)
+        elif len(node.orelse) != 0:
+            logging.error("Orelse construct of for loops is not supported.")
+            exit(1)
+
+        # case range(stop) -> [0, stop)
+        if len(node.iter.args) == 1:
+            start_val = self.builder.make_literal(0)
+            stop_val = self.visit(node.iter.args[0])
+        # cases range(start, stop[, step]) -> [start, stop)
+        else:
+            start_val = self.visit(node.iter.args[0])
+            stop_val = self.visit(node.iter.args[1])
+
+        # case range(start, stop, step)
+        if len(node.iter.args) == 3:
+            step_val = self.visit(node.iter.args[2])
+        else:
+            step_val = self.builder.make_literal(1)
+
+        # Currently, the initializer is a block with a single statement.
+        var_decl = self.builder.make_variable_declaration(target, start_val)
+
+        # TODO: We force set the type of the for-loop variable to integer. This is not automatically the case, since
+        # variable assignments loose type information (see frontend-limitations markdown).
+        var_decl["datatype"] = "int"
+
+        initializer = self.builder.make_block([var_decl])
+
+        # Python for loops with range only perform addition, the Pythonic way to do more complex conditions is usually
+        # to use while loops.
+        update = self.builder.make_update(target, self.builder.constants.ADD, step_val)
+
+        # If start > stop, the condition is target > stop. Otherwise, it is target < stop.
+        start_lte_stop = self.builder.make_binary_expression(start_val, self.builder.constants.LTE, stop_val)
+        start_gt_stop = self.builder.make_binary_expression(start_val, self.builder.constants.GT, stop_val)
+        target_lt_stop = self.builder.make_binary_expression(target, self.builder.constants.LT, stop_val)
+        target_gt_stop = self.builder.make_binary_expression(target, self.builder.constants.GT, stop_val)
+
+        condition_case_1 = self.builder.make_binary_expression(start_lte_stop, self.builder.constants.AND,
+                                                               target_lt_stop)
+        condition_case_2 = self.builder.make_binary_expression(start_gt_stop, self.builder.constants.AND,
+                                                               target_gt_stop)
+        condition = self.builder.make_binary_expression(condition_case_1, self.builder.constants.OR, condition_case_2)
+
+        stmts = list(map(self.visit, node.body))
+        body = self.builder.make_block(stmts)
+
+        return self.builder.make_for(initializer, condition, update, body)
+
+    def visit_FunctionDef(self, node: FunctionDef) -> dict:
+        """
+        Visit a Python function definition and convert it to an AST function definition.
+        The 'main' function is treated differently: it is removed and it's contents are
+        directly converted to the root of the AST.
+        """
+
+        if node.name == MAIN_SYMBOL:
+            # TODO: From Python 3.9 onwards, we can use unparse instead of dump (newly added to ast)
+            # logging.debug(f"Parsing python main function:\n{unparse(node)}")
+
+            last_stmt = node.body[-1]
+            if not isinstance(last_stmt, Return):
+                logging.error("The FHE main function has to return something.")
+                exit(1)
+
+            # Parse the main function, except the return statement
+            stmts = list(map(self.visit, node.body))
+            body = self.builder.make_block(stmts)
+
+            # Parse the return statement separately. We do support parsing multiple variables of the same type,
+            # but we don't support expressions.
+            ret_vals = last_stmt.value.elts if isinstance(last_stmt.value, Tuple) else [last_stmt.value]
+            ret_vars = dict()
+            ret_constants = []
+            for i, ret_val in enumerate(ret_vals):
+                if isinstance(ret_val, Name):
+                    ret_vars[ret_val.id] = i
+                elif isinstance(ret_val, Constant):
+                    ret_constants.append(ret_val.value)
+
+            self.prog.add_main_fn(body, self._args_to_dict(node.args), ret_vars, ret_constants)
+
+            logging.debug(f"... to ABC AST:\n{json.dumps(body, indent=2)}")
+            return {}
+        else:
+            logging.error(UNSUPPORTED_FUNCTION, node.name)
+            exit(1)
+
+    def visit_Gt(self, node: Gt) -> dict:
+        return self.builder.constants.GT
+
+    def visit_GtE(self, node: GtE) -> dict:
+        return self.builder.constants.GTE
+
+    def visit_Index(self, node: Index) -> dict:
+        # TODO: so far, all Index nodes that we used only had the single attribute "value"
+        return self.visit(node.value)
+
+    def visit_List(self, node: List) -> dict:
+        """
+        Visit a Python list and convert it to an AST ExpressionList.
+        """
+        # TODO: at the moment, python lists are translated to ABC ExpressionLists. We potentially need to give up some
+        #   list features if we keep it like this (list comprehension, list concatenation).
+
+        exprs = list(map(self.visit, node.elts))
+        return self.builder.make_expression_list(exprs)
+
+    def visit_Lt(self, node: Lt) -> dict:
+        return self.builder.constants.LT
+
+    def visit_LtE(self, node: LtE) -> dict:
+        return self.builder.constants.LTE
+
+    def visit_Mod(self, node: Mod) -> dict:
+        return self.builder.constants.MOD
+
+    def visit_Mult(self, node: Mult) -> dict:
+        return self.builder.constants.MUL
+
+    def visit_Name(self, node: Name) -> dict:
+        """
+        Visit a Python name node and convert it to an ABC AST Variable node.
+        """
+
+        return self.builder.make_variable(node.id)
+
+    def visit_Or(self, node: Or) -> dict:
+        return self.builder.constants.OR
+
+    def visit_Return(self, node: Return) -> dict:
+        """
+        Visit a Python return statement, convert its value to an ABC node and return an ABC Return node.
+        """
+
+        # Only support single return statements, since we don't support multi-value return statements yet
+        if isinstance(node.value, Tuple):
+            # TODO: From Python 3.9 onwards, we can use unparse instead of dump (newly added to ast)
+            logging.error(UNSUPPORTED_MULTI_VALUE_RETURN, dump(node))
+            exit(1)
+
+        ret_node = self.visit(node.value)
+
+        return self.builder.make_return(ret_node)
+
+    def visit_Sub(self, node: Sub) -> dict:
+        return self.builder.constants.SUB
+
+    def visit_Subscript(self, node: Subscript) -> dict:
+        target = self.visit(node.value)
+
+        # Slice accesses will throw an error, since the Slice type is not implemented.
+        index = self.visit(node.slice)
+
+        return self.builder.make_index_access(target, index)
+
+    #
+    # Unsupported visit functions
+    #
+
+    def visit_Assert(self, node: Assert) -> dict:
+        logging.error(UNSUPPORTED_STATEMENT, type(node))
+        exit(1)
+
+    def visit_Attribute(self, node: Attribute) -> dict:
+        logging.error(UNSUPPORTED_STATEMENT, type(node))
+        exit(1)
+
+    def visit_Await(self, node: Await) -> dict:
+        logging.error(UNSUPPORTED_STATEMENT, type(node))
+        exit(1)
+
+    def visit_AnnAssign(self, node: AnnAssign) -> dict:
+        logging.error(UNSUPPORTED_STATEMENT, type(node))
+        exit(1)
+
+    def visit_AsyncFor(self, node: AsyncFor) -> dict:
+        logging.error(UNSUPPORTED_STATEMENT, type(node))
+        exit(1)
+
+    def visit_AsyncFunctionDef(self, node: AsyncFunctionDef) -> dict:
+        logging.error(UNSUPPORTED_STATEMENT, type(node))
+        exit(1)
+
+    def visit_AsyncWith(self, node: AsyncWith) -> dict:
+        logging.error(UNSUPPORTED_STATEMENT, type(node))
+        exit(1)
+
+    def visit_AugLoad(self, node: AugLoad) -> dict:
+        logging.error(UNSUPPORTED_STATEMENT, type(node))
+        exit(1)
+
+    def visit_AugStore(self, node: AugStore) -> dict:
+        logging.error(UNSUPPORTED_STATEMENT, type(node))
+        exit(1)
+
+    def visit_BoolOp(self, node: BoolOp) -> dict:
+        logging.error(UNSUPPORTED_STATEMENT, type(node))
+        exit(1)
+
+    def visit_BitAnd(self, node: BitAnd) -> dict:
+        logging.error(UNSUPPORTED_STATEMENT, type(node))
+        exit(1)
+
+    def visit_BitOr(self, node: BitOr) -> dict:
+        logging.error(UNSUPPORTED_STATEMENT, type(node))
+        exit(1)
+
+    def visit_BitXor(self, node: BitXor) -> dict:
+        logging.error(UNSUPPORTED_STATEMENT, type(node))
+        exit(1)
+
+    def visit_Break(self, node: Break) -> dict:
+        logging.error(UNSUPPORTED_STATEMENT, type(node))
+        exit(1)
+
+    def visit_Bytes(self, node: Bytes) -> dict:
+        logging.error(UNSUPPORTED_STATEMENT, type(node))
+        exit(1)
+
+    def visit_ClassDef(self, node: ClassDef) -> dict:
+        logging.error(UNSUPPORTED_STATEMENT, type(node))
+        exit(1)
+
+    def visit_Compare(self, node: Compare) -> dict:
+        logging.error(UNSUPPORTED_STATEMENT, type(node))
+        exit(1)
+
+    def visit_Continue(self, node: Continue) -> dict:
+        logging.error(UNSUPPORTED_STATEMENT, type(node))
+        exit(1)
+
+    def visit_Del(self, node: Del) -> dict:
+        logging.error(UNSUPPORTED_STATEMENT, type(node))
+        exit(1)
+
+    def visit_Delete(self, node: Delete) -> dict:
+        logging.error(UNSUPPORTED_STATEMENT, type(node))
+        exit(1)
+
+    def visit_Dict(self, node: Dict) -> dict:
+        logging.error(UNSUPPORTED_STATEMENT, type(node))
+        exit(1)
+
+    def visit_DictComp(self, node: DictComp) -> dict:
+        logging.error(UNSUPPORTED_STATEMENT, type(node))
+        exit(1)
+
+    def visit_Ellipsis(self, node: Ellipsis) -> dict:
+        logging.error(UNSUPPORTED_STATEMENT, type(node))
+        exit(1)
+
+    def visit_ExceptHandler(self, node: ExceptHandler) -> dict:
+        logging.error(UNSUPPORTED_STATEMENT, type(node))
+        exit(1)
+
+    def visit_Expr(self, node: Expr) -> dict:
+        logging.error(UNSUPPORTED_STATEMENT, type(node))
+        exit(1)
+
+    def visit_Expression(self, node: Expression) -> dict:
+        logging.error(UNSUPPORTED_STATEMENT, type(node))
+        exit(1)
+
+    def visit_ExtSlice(self, node: ExtSlice) -> dict:
+        logging.error(UNSUPPORTED_STATEMENT, type(node))
+        exit(1)
+
+    def visit_FormattedValue(self, node: FormattedValue) -> dict:
+        logging.error(UNSUPPORTED_STATEMENT, type(node))
+        exit(1)
+
+    def visit_GeneratorExp(self, node: GeneratorExp) -> dict:
+        logging.error(UNSUPPORTED_STATEMENT, type(node))
+        exit(1)
+
+    def visit_Global(self, node: Global) -> dict:
+        logging.error(UNSUPPORTED_STATEMENT, type(node))
+        exit(1)
+
+    def visit_If(self, node: If) -> dict:
+        logging.error(UNSUPPORTED_STATEMENT, type(node))
+        exit(1)
+
+    def visit_IfExp(self, node: IfExp) -> dict:
+        logging.error(UNSUPPORTED_STATEMENT, type(node))
+        exit(1)
+
+    def visit_Import(self, node: Import) -> dict:
+        logging.error(UNSUPPORTED_STATEMENT, type(node))
+        exit(1)
+
+    def visit_ImportFrom(self, node: ImportFrom) -> dict:
+        logging.error(UNSUPPORTED_STATEMENT, type(node))
+        exit(1)
+
+    def visit_In(self, node: In) -> dict:
+        logging.error(UNSUPPORTED_STATEMENT, type(node))
+        exit(1)
+
+    def visit_Interactive(self, node: Interactive) -> dict:
+        logging.error(UNSUPPORTED_STATEMENT, type(node))
+        exit(1)
+
+    def visit_Invert(self, node: Invert) -> dict:
+        logging.error(UNSUPPORTED_STATEMENT, type(node))
+        exit(1)
+
+    def visit_Is(self, node: Is) -> dict:
+        logging.error(UNSUPPORTED_STATEMENT, type(node))
+        exit(1)
+
+    def visit_IsNot(self, node: IsNot) -> dict:
+        logging.error(UNSUPPORTED_STATEMENT, type(node))
+        exit(1)
+
+    def visit_JoinedStr(self, node: JoinedStr) -> dict:
+        logging.error(UNSUPPORTED_STATEMENT, type(node))
+        exit(1)
+
+    def visit_Lambda(self, node: Lambda) -> dict:
+        logging.error(UNSUPPORTED_STATEMENT, type(node))
+        exit(1)
+
+    def visit_ListComp(self, node: ListComp) -> dict:
+        logging.error(UNSUPPORTED_STATEMENT, type(node))
+        exit(1)
+
+    def visit_Load(self, node: Load) -> dict:
+        logging.error(UNSUPPORTED_STATEMENT, type(node))
+        exit(1)
+
+    def visit_LShift(self, node: LShift) -> dict:
+        logging.error(UNSUPPORTED_STATEMENT, type(node))
+        exit(1)
+
+    def visit_MatMult(self, node: MatMult) -> dict:
+        logging.error(UNSUPPORTED_STATEMENT, type(node))
+        exit(1)
+
+    def visit_Module(self, node: Module) -> dict:
+        logging.error(UNSUPPORTED_STATEMENT, type(node))
+        exit(1)
+
+    def visit_NameConstant(self, node: NameConstant) -> dict:
+        logging.error(UNSUPPORTED_STATEMENT, type(node))
+        exit(1)
+
+    def visit_NamedExpr(self, node: NamedExpr) -> dict:
+        logging.error(UNSUPPORTED_STATEMENT, type(node))
+        exit(1)
+
+    def visit_Nonlocal(self, node: Nonlocal) -> dict:
+        logging.error(UNSUPPORTED_STATEMENT, type(node))
+        exit(1)
+
+    def visit_Not(self, node: Not) -> dict:
+        logging.error(UNSUPPORTED_STATEMENT, type(node))
+        exit(1)
+
+    def visit_NotEq(self, node: NotEq) -> dict:
+        logging.error(UNSUPPORTED_STATEMENT, type(node))
+        exit(1)
+
+    def visit_NotIn(self, node: NotIn) -> dict:
+        logging.error(UNSUPPORTED_STATEMENT, type(node))
+        exit(1)
+
+    def visit_Num(self, node: Num) -> dict:
+        logging.error(UNSUPPORTED_STATEMENT, type(node))
+        exit(1)
+
+    def visit_Param(self, node: Param) -> dict:
+        logging.error(UNSUPPORTED_STATEMENT, type(node))
+        exit(1)
+
+    def visit_Pass(self, node: Pass) -> dict:
+        logging.error(UNSUPPORTED_STATEMENT, type(node))
+        exit(1)
+
+    def visit_Pow(self, node: Pow) -> dict:
+        logging.error(UNSUPPORTED_STATEMENT, type(node))
+        exit(1)
+
+    def visit_Raise(self, node: Raise) -> dict:
+        logging.error(UNSUPPORTED_STATEMENT, type(node))
+        exit(1)
+
+    def visit_RShift(self, node: RShift) -> dict:
+        logging.error(UNSUPPORTED_STATEMENT, type(node))
+        exit(1)
+
+    def visit_Set(self, node: Set) -> dict:
+        logging.error(UNSUPPORTED_STATEMENT, type(node))
+        exit(1)
+
+    def visit_SetComp(self, node: SetComp) -> dict:
+        logging.error(UNSUPPORTED_STATEMENT, type(node))
+        exit(1)
+
+    def visit_Slice(self, node: Slice) -> dict:
+        logging.error(UNSUPPORTED_STATEMENT, type(node))
+        exit(1)
+
+    def visit_Starred(self, node: Starred) -> dict:
+        logging.error(UNSUPPORTED_STATEMENT, type(node))
+        exit(1)
+
+    def visit_Store(self, node: Store) -> dict:
+        logging.error(UNSUPPORTED_STATEMENT, type(node))
+        exit(1)
+
+    def visit_Str(self, node: Str) -> dict:
+        logging.error(UNSUPPORTED_STATEMENT, type(node))
+        exit(1)
+
+    def visit_Suite(self, node: Suite) -> dict:
+        logging.error(UNSUPPORTED_STATEMENT, type(node))
+        exit(1)
+
+    def visit_Try(self, node: Try) -> dict:
+        logging.error(UNSUPPORTED_STATEMENT, type(node))
+        exit(1)
+
+    def visit_Tuple(self, node: Tuple) -> dict:
+        logging.error(UNSUPPORTED_STATEMENT, type(node))
+        exit(1)
+
+    def visit_UAdd(self, node: UAdd) -> dict:
+        logging.error(UNSUPPORTED_STATEMENT, type(node))
+        exit(1)
+
+    def visit_UnaryOp(self, node: UnaryOp) -> dict:
+        logging.error(UNSUPPORTED_STATEMENT, type(node))
+        exit(1)
+
+    def visit_USub(self, node: USub) -> dict:
+        logging.error(UNSUPPORTED_STATEMENT, type(node))
+        exit(1)
+
+    def visit_While(self, node: While) -> dict:
+        logging.error(UNSUPPORTED_STATEMENT, type(node))
+        exit(1)
+
+    def visit_With(self, node: With) -> dict:
+        logging.error(UNSUPPORTED_STATEMENT, type(node))
+        exit(1)
+
+    def visit_Yield(self, node: Yield) -> dict:
+        logging.error(UNSUPPORTED_STATEMENT, type(node))
+        exit(1)
+
+    def visit_YieldFrom(self, node: YieldFrom) -> dict:
+        logging.error(UNSUPPORTED_STATEMENT, type(node))
+        exit(1)
diff --git a/python/pyabc/CMakeLists.txt b/python/pyabc/CMakeLists.txt
new file mode 100644
index 00000000..c62af80b
--- /dev/null
+++ b/python/pyabc/CMakeLists.txt
@@ -0,0 +1,5 @@
+# pybind11: add our wrapper for ABC
+pybind11_add_module(pyabc abc_wrapper.cpp)
+target_link_libraries(pyabc PRIVATE ast_opt)
+
+set_target_properties(pyabc PROPERTIES OUTPUT_NAME _abc_wrapper)
\ No newline at end of file
diff --git a/python/pyabc/FunctionVisitor.py b/python/pyabc/FunctionVisitor.py
new file mode 100644
index 00000000..9959417f
--- /dev/null
+++ b/python/pyabc/FunctionVisitor.py
@@ -0,0 +1,16 @@
+
+from ast import *
+
+class FunctionVisitor(NodeVisitor):
+    """
+    Visitor to search a function definition in the given AST and return it's AST if found.
+    """
+
+    def __init__(self, target_fn, *args, **kwargs):
+        self.target_fn = target_fn
+        super(FunctionVisitor, self).__init__(*args, **kwargs)
+
+    def visit_FunctionDef(self, node: FunctionDef):
+        if node.name == self.target_fn:
+            return node
+        return None
\ No newline at end of file
diff --git a/python/pyabc/__init__.py b/python/pyabc/__init__.py
new file mode 100644
index 00000000..dae84945
--- /dev/null
+++ b/python/pyabc/__init__.py
@@ -0,0 +1,3 @@
+from .ABCContext import ABCContext
+from .ABCProgram import ABCProgram
+from .ABCTypes import *
\ No newline at end of file
diff --git a/python/pyabc/abc_wrapper.cpp b/python/pyabc/abc_wrapper.cpp
new file mode 100644
index 00000000..f43cddd0
--- /dev/null
+++ b/python/pyabc/abc_wrapper.cpp
@@ -0,0 +1,112 @@
+#include <pybind11/pybind11.h>
+#include <pybind11/stl.h>
+#include <ast_opt/parser/Parser.h>
+#include <ast_opt/parser/Errors.h>
+#include <ast_opt/visitor/ProgramPrintVisitor.h>
+#include <ast_opt/runtime/DummyCiphertext.h>
+#include "ast_opt/compiler/Compiler.h"
+#include "ast_opt/runtime/Cleartext.h"
+
+namespace py = pybind11;
+using json = nlohmann::json;
+
+/// Class storing a program and intermediate state. E.g., it stores a pre-compiled program AST to avoid
+/// having to pass a unique pointer to Python.
+class ABCProgramWrapper {
+ private:
+  std::unique_ptr<AbstractNode> programAst;
+  json programArgs;
+
+  /// Helper function to extract literal (array) and push it to the results vector
+  template<class T>
+  static void extract_literal_result_vec(AbstractValue *cipherClearText,
+                                         std::vector<py::object> &result_vec, bool &success) {
+
+    if (auto cleartextInt = dynamic_cast<Cleartext<T> *>(cipherClearText)) {   // result is a cleartext
+      success = true;
+      auto cleartextData = py::cast(cleartextInt->getData());
+      result_vec.push_back(cleartextData);
+    }
+  }
+
+  static void extract_ciphertext_result_vec(AbstractValue *resultCiphertext,
+                                            std::vector<py::object> &result_vec, bool &success) {
+
+    if (auto ciphertext = dynamic_cast<AbstractCiphertext *>(resultCiphertext)) {
+      // TODO: we only support dummy ciphertext values. Currently, we cannot pass encrypted data to Python.
+      //  do key management and/or export the ciphertext to Python.
+      auto scf = std::make_unique<DummyCiphertextFactory>();
+
+      // TODO: there's only support for int64_t values in the dummy ciphertext factory
+      std::vector<int64_t> result;
+      scf->decryptCiphertext(*ciphertext, result);
+
+      result_vec.push_back(py::cast(result));
+      success = true;
+    }
+  }
+
+ public:
+  /// Create a program and pre-process the given JSON to a proper ABC AST.
+  /// \param program JSON version of ABC AST
+  explicit ABCProgramWrapper(const std::string program, const std::string args) {
+    programAst = Parser::parseJson(program);
+    // TODO: where can we mark arguments as secret? The args argument has a boolen for those that should be secret, but do
+    //    we have to already mark them while/before parsing the AST or only when executing?
+    programArgs = json::parse(args);
+  }
+
+  /// Execute the compiled program on the given inputs and outputs
+  // XXX: This assumes the result is in clear text.
+  // We use python types as result values to be able to return different types without having to use templates,
+  // since binding templates would require us to instantiate different programs depending on their return type.
+  py::tuple execute(std::string inputs, std::vector<std::string> outputIdentifiers) {
+    // Cloning the programAst is necessary to be able to execute the same program multiple times (which uses the same
+    // program AST)
+    auto result = Compiler::compileJson(programAst->clone(), inputs, outputIdentifiers);
+    std::vector<py::object> result_vec;
+
+    for (const auto &[identifier, cipherClearText] : result) {
+      bool success = false;
+      auto ciphertextValue = cipherClearText.get();
+
+      // Plaintext values
+      extract_literal_result_vec<bool>(ciphertextValue, result_vec, success);
+      extract_literal_result_vec<int>(ciphertextValue, result_vec, success);
+      extract_literal_result_vec<float>(ciphertextValue, result_vec, success);
+      extract_literal_result_vec<double>(ciphertextValue, result_vec, success);
+      extract_literal_result_vec<char>(ciphertextValue, result_vec, success);
+      extract_literal_result_vec<std::string>(ciphertextValue, result_vec, success);
+
+      // Ciphertext value
+      extract_ciphertext_result_vec(ciphertextValue, result_vec, success);
+
+      if (!success)
+        throw stork::runtime_error("Currently, only the dummy ciphertext factory and cleartext results are implemented!");
+    }
+    return py::cast(result_vec);
+  }
+
+  /// Convert the ABC AST to CPP pseudo-code
+  std::string to_cpp_string() {
+    std::stringstream ss;
+    ProgramPrintVisitor v(ss);
+    programAst->accept(v);
+    return ss.str();
+  }
+};
+
+PYBIND11_MODULE(_abc_wrapper, m) {
+  m.doc() = "Wrapper to export ABC's functionality to read and execute an AST from a JSON file.";
+
+  py::class_<ABCProgramWrapper>(m, "ABCProgramWrapper")
+      .def(py::init<const std::string, const std::string>(),
+          "Create a program and pre-process the given JSON version of an ABC AST to the CPP version of it."\
+          "The second argument is a JSON dump of the arguments and their secret tainting: "\
+          "var_name = {\"opt\": Bool, \"value\": value, \"secret\": Bool}"\
+          "See _args_to_dict in ABCVisitor.")
+      .def("execute", &ABCProgramWrapper::execute,
+           "Execute the compiled program on the given inputs and outputs")
+      .def("to_cpp_string", &ABCProgramWrapper::to_cpp_string,
+           "Convert the ABC AST to CPP pseudo-code");
+}
diff --git a/python/setup.py.in b/python/setup.py.in
new file mode 100644
index 00000000..11f4bf5c
--- /dev/null
+++ b/python/setup.py.in
@@ -0,0 +1,21 @@
+from setuptools import setup, find_packages
+from setuptools.dist import Distribution
+
+# Based on https://github.com/microsoft/EVA/blob/main/python/setup.py.in (visited September 2021)
+class BinaryDistribution(Distribution):
+    """Distribution which always forces a binary package with platform name"""
+    def has_ext_modules(foo):
+        return True
+
+setup(
+    name='pyabc',
+    version='${PROJECT_VERSION}',
+    author='PPS Lab',
+    author_email='alexander.viand@inf.ethz.ch, miro.haller@inf.ethz.ch',
+    description='Python frontend for the ABC compiler for fully homomorphic encryption.',
+    packages=find_packages('${CMAKE_CURRENT_BINARY_DIR}'),
+    distclass=BinaryDistribution,
+    package_data={
+        'pyabc': ['$<TARGET_FILE_NAME:pyabc>'],
+    }
+)
\ No newline at end of file
diff --git a/src/ast/Assignment.cpp b/src/ast/Assignment.cpp
index 8029f88d..a1767d95 100644
--- a/src/ast/Assignment.cpp
+++ b/src/ast/Assignment.cpp
@@ -1,3 +1,4 @@
+#include <ast_opt/parser/Parser.h>
 #include "ast_opt/ast/Assignment.h"
 #include "ast_opt/ast/Variable.h"
 #include "ast_opt/utilities/IVisitor.h"
@@ -134,6 +135,13 @@ nlohmann::json Assignment::toJson() const {
   return j;
 }
 
+std::unique_ptr<Assignment> Assignment::fromJson(nlohmann::json j) {
+  auto target = Parser::parseJsonTarget(j["target"]);
+  auto value = Parser::parseJsonExpression(j["value"]);
+
+  return std::make_unique<Assignment>(std::move(target), std::move(value));
+}
+
 std::string Assignment::toString(bool printChildren) const {
   return AbstractNode::toStringHelper(printChildren, {});
 }
diff --git a/src/ast/BinaryExpression.cpp b/src/ast/BinaryExpression.cpp
index d665949f..f309b3ac 100644
--- a/src/ast/BinaryExpression.cpp
+++ b/src/ast/BinaryExpression.cpp
@@ -1,4 +1,5 @@
 #include <vector>
+#include <ast_opt/parser/Parser.h>
 #include "ast_opt/ast/BinaryExpression.h"
 #include "ast_opt/utilities/IVisitor.h"
 
@@ -142,6 +143,22 @@ nlohmann::json BinaryExpression::toJson() const {
   return j;
 }
 
+std::unique_ptr<BinaryExpression> BinaryExpression::fromJson(nlohmann::json j) {
+  // Parse operation
+  auto op = fromStringToOperatorVariant(j["operator"]);
+
+  // Parse operands (if available)
+  std::unique_ptr<AbstractExpression> left, right;
+
+  if (j.find("left") != j.end())
+    left = Parser::parseJsonExpression(j["left"]);
+
+  if (j.find("right") != j.end())
+    right = Parser::parseJsonExpression(j["right"]);
+
+  return std::make_unique<BinaryExpression>(std::move(left), op, std::move(right));
+}
+
 std::string BinaryExpression::toString(bool printChildren) const {
   return AbstractNode::toStringHelper(printChildren, {op.toString()});
 
diff --git a/src/ast/Block.cpp b/src/ast/Block.cpp
index 18f5634e..261ec84f 100644
--- a/src/ast/Block.cpp
+++ b/src/ast/Block.cpp
@@ -1,5 +1,6 @@
 #include <iostream>
 #include <exception>
+#include <ast_opt/parser/Parser.h>
 #include "ast_opt/ast/Block.h"
 #include "ast_opt/utilities/IVisitor.h"
 
@@ -151,6 +152,14 @@ nlohmann::json Block::toJson() const {
   return j;
 }
 
+std::unique_ptr<Block> Block::fromJson(nlohmann::json j) {
+  std::vector<std::unique_ptr<AbstractStatement>> statements;
+  for (auto statement : j["statements"]) {
+    statements.emplace_back(Parser::parseJsonStatement(statement));
+  }
+  return std::make_unique<Block>(std::move(statements));
+}
+
 std::string Block::toString(bool printChildren) const {
   return AbstractNode::toStringHelper(printChildren, {});
 }
diff --git a/src/ast/ExpressionList.cpp b/src/ast/ExpressionList.cpp
index 304159ef..1e0fbe9b 100644
--- a/src/ast/ExpressionList.cpp
+++ b/src/ast/ExpressionList.cpp
@@ -1,5 +1,6 @@
 #include <iostream>
 #include <exception>
+#include <ast_opt/parser/Parser.h>
 #include "ast_opt/ast/ExpressionList.h"
 #include "ast_opt/utilities/IVisitor.h"
 
@@ -147,6 +148,14 @@ nlohmann::json ExpressionList::toJson() const {
   return j;
 }
 
+std::unique_ptr<ExpressionList> ExpressionList::fromJson(nlohmann::json j) {
+  std::vector<std::unique_ptr<AbstractExpression>> expressions;
+  for (auto expression : j["expressions"]) {
+    expressions.emplace_back(Parser::parseJsonExpression(expression));
+  }
+  return std::make_unique<ExpressionList>(std::move(expressions));
+}
+
 std::string ExpressionList::toString(bool printChildren) const {
   return AbstractNode::toStringHelper(printChildren, {});
 }
diff --git a/src/ast/For.cpp b/src/ast/For.cpp
index 277553fd..310988d6 100644
--- a/src/ast/For.cpp
+++ b/src/ast/For.cpp
@@ -1,3 +1,5 @@
+#include <ast_opt/parser/Parser.h>
+#include <ast_opt/utilities/NodeUtils.h>
 #include "ast_opt/ast/For.h"
 #include "ast_opt/utilities/IVisitor.h"
 
@@ -184,6 +186,19 @@ nlohmann::json For::toJson() const {
   return j;
 }
 
+std::unique_ptr<For> For::fromJson(nlohmann::json j) {
+  auto initializerStmt = (j.contains("initializer")) ? Parser::parseJsonStatement(j["initializer"]) : nullptr;
+  auto conditionExpr = (j.contains("condition")) ? Parser::parseJsonExpression(j["condition"]) : nullptr;
+  auto updateStmt = (j.contains("update")) ? Parser::parseJsonStatement(j["update"]) : nullptr;
+  auto bodyStmt = (j.contains("body")) ? Parser::parseJsonStatement(j["body"]) : nullptr;
+
+  auto initializerBlock = castUniquePtr<AbstractStatement, Block>(std::move(initializerStmt));
+  auto updateBlock = castUniquePtr<AbstractStatement, Block>(std::move(updateStmt));
+  auto bodyBlock = castUniquePtr<AbstractStatement, Block>(std::move(bodyStmt));
+
+  return std::make_unique<For>(std::move(initializerBlock), std::move(conditionExpr), std::move(updateBlock), std::move(bodyBlock));
+}
+
 std::string For::toString(bool printChildren) const {
   return AbstractNode::toStringHelper(printChildren, {});
 }
diff --git a/src/ast/IndexAccess.cpp b/src/ast/IndexAccess.cpp
index 34306106..4b57b9c8 100644
--- a/src/ast/IndexAccess.cpp
+++ b/src/ast/IndexAccess.cpp
@@ -1,3 +1,4 @@
+#include <ast_opt/parser/Parser.h>
 #include "ast_opt/ast/IndexAccess.h"
 #include "ast_opt/utilities/IVisitor.h"
 
@@ -121,6 +122,12 @@ nlohmann::json IndexAccess::toJson() const {
   return j;
 }
 
+std::unique_ptr<IndexAccess> IndexAccess::fromJson(nlohmann::json j) {
+  auto target = Parser::parseJsonTarget(j["target"]);
+  auto index = Parser::parseJsonExpression(j["index"]);
+  return std::make_unique<IndexAccess>(std::move(target), std::move(index));
+}
+
 std::string IndexAccess::toString(bool printChildren) const {
   return AbstractNode::toStringHelper(printChildren, {});
 }
diff --git a/src/ast/Return.cpp b/src/ast/Return.cpp
index 39b262ce..7dff87f4 100644
--- a/src/ast/Return.cpp
+++ b/src/ast/Return.cpp
@@ -1,4 +1,5 @@
 #include <utility>
+#include <ast_opt/parser/Parser.h>
 #include "ast_opt/ast/Return.h"
 #include "ast_opt/utilities/IVisitor.h"
 
@@ -87,6 +88,11 @@ nlohmann::json Return::toJson() const {
   return j;
 }
 
+std::unique_ptr<Return> Return::fromJson(nlohmann::json j) {
+  auto expression = Parser::parseJsonExpression(j["value"]);
+  return std::make_unique<Return>(std::move(expression));
+}
+
 std::string Return::toString(bool printChildren) const {
   return AbstractNode::toStringHelper(printChildren, {});
 }
diff --git a/src/ast/Variable.cpp b/src/ast/Variable.cpp
index a28ad622..9928ca98 100644
--- a/src/ast/Variable.cpp
+++ b/src/ast/Variable.cpp
@@ -67,6 +67,10 @@ nlohmann::json Variable::toJson() const {
   return j;
 }
 
+std::unique_ptr<Variable> Variable::fromJson(nlohmann::json j) {
+  return std::make_unique<Variable>(j["identifier"].get<std::string>());
+}
+
 std::string Variable::toString(bool printChildren) const {
   return AbstractNode::toStringHelper(printChildren, {getIdentifier()});
 }
diff --git a/src/ast/VariableDeclaration.cpp b/src/ast/VariableDeclaration.cpp
index 87d13eeb..09b686ba 100644
--- a/src/ast/VariableDeclaration.cpp
+++ b/src/ast/VariableDeclaration.cpp
@@ -1,4 +1,6 @@
 #include <utility>
+#include <ast_opt/parser/Parser.h>
+#include <ast_opt/parser/Errors.h>
 #include "ast_opt/ast/VariableDeclaration.h"
 #include "ast_opt/utilities/IVisitor.h"
 
@@ -142,6 +144,15 @@ nlohmann::json VariableDeclaration::toJson() const {
   return j;
 }
 
+std::unique_ptr<VariableDeclaration> VariableDeclaration::fromJson(nlohmann::json j) {
+
+  VariableDeclaration value(Datatype(j["datatype"].get<std::string>()),
+                            Variable::fromJson(j["target"]),
+                            Parser::parseJsonExpression(j["value"]));
+
+  return std::make_unique<VariableDeclaration>(std::move(value));
+}
+
 std::string VariableDeclaration::toString(bool printChildren) const {
   return AbstractNode::toStringHelper(printChildren, {datatype.toString()});
 }
diff --git a/src/compiler/Compiler.cpp b/src/compiler/Compiler.cpp
new file mode 100644
index 00000000..d49d9274
--- /dev/null
+++ b/src/compiler/Compiler.cpp
@@ -0,0 +1,94 @@
+#include <ast_opt/visitor/TypeCheckingVisitor.h>
+#include <ast_opt/runtime/RuntimeVisitor.h>
+#include <ast_opt/parser/Parser.h>
+#include <ast_opt/runtime/DummyCiphertextFactory.h>
+#include <ast_opt/parser/Errors.h>
+#include <ast_opt/compiler/Compiler.h>
+#include <ast_opt/utilities/NodeUtils.h>
+
+OutputIdentifierValuePairs Compiler::compileAst(std::unique_ptr<AbstractNode> programAst,
+                                                std::unique_ptr<Block> inputBlock,
+                                                std::unique_ptr<AbstractNode> outputAst) {
+
+  auto scf = std::make_unique<DummyCiphertextFactory>();
+  auto tcv = std::make_unique<TypeCheckingVisitor>();
+
+  // create and prepopulate TypeCheckingVisitor
+  auto registerInputVariable = [&tcv](Scope &rootScope, const std::string &identifier, Datatype datatype) {
+    auto scopedIdentifier = std::make_unique<ScopedIdentifier>(rootScope, identifier);
+    rootScope.addIdentifier(identifier);
+    tcv->addVariableDatatype(*scopedIdentifier, datatype);
+  };
+
+  // Parse the input block and add detected variables to the root scope.
+  auto rootScope = std::make_unique<Scope>(*programAst);
+
+  for (auto stmt : inputBlock->getStatements()) {
+    if (auto variable_declaration = dynamic_cast<VariableDeclaration *>(&stmt.get())) {
+      if (auto variable = dynamic_cast<Variable *>(&variable_declaration->getTarget())) {
+        registerInputVariable(*rootScope,
+                              variable->getIdentifier(),
+                              variable_declaration->getDatatype());
+      }
+      else {
+        throw stork::runtime_error("Invalid input block: all statements must contain a variable name on the LHS.");
+      }
+    }
+    else {
+      throw stork::runtime_error("Invalid input block: must only consists of variable declarations.");
+    }
+  }
+
+  tcv->setRootScope(std::move(rootScope));
+  programAst->accept(*tcv);
+
+  // run the program and get its output
+  auto secretTaintedNodesMap = tcv->getSecretTaintedNodes();
+  // create a SpecialRuntimeVisitor instance
+  RuntimeVisitor srv(*scf, *inputBlock, secretTaintedNodesMap);
+  srv.executeAst(*programAst);
+
+  return srv.getOutput(*outputAst);
+}
+
+std::unique_ptr<AbstractNode> Compiler::buildOutputBlock(std::vector<std::string> outputIdentifiers) {
+  std::vector<std::unique_ptr<AbstractStatement>> statements;
+
+  for (auto identifier : outputIdentifiers) {
+    auto assignment = std::make_unique<Assignment>(std::make_unique<Variable>(identifier), std::make_unique<Variable>(identifier));
+    statements.emplace_back(std::move(assignment));
+  }
+  return std::make_unique<Block>(std::move(statements));
+}
+
+OutputIdentifierValuePairs Compiler::compile(std::string program,
+                                             std::string input,
+                                             std::vector<std::string> outputIdentifiers) {
+
+  auto programAst = Parser::parse(program);
+  auto inputAst = Parser::parse(input);
+  auto outputAst = Compiler::buildOutputBlock(outputIdentifiers);
+
+  // The input must be a block by convention, otherwise, throw an error
+  auto inputBlock = castUniquePtr<AbstractNode, Block>(std::move(inputAst));
+
+  return Compiler::compileAst(std::move(programAst), std::move(inputBlock), std::move(outputAst));
+}
+
+OutputIdentifierValuePairs Compiler::compileJson(std::string program,
+                                                 std::string input,
+                                                 std::vector<std::string> outputIdentifiers) {
+  return Compiler::compileJson(Parser::parseJson(program), input, outputIdentifiers);
+}
+
+OutputIdentifierValuePairs Compiler::compileJson(std::unique_ptr<AbstractNode> programAst,
+                                                 std::string input,
+                                                 std::vector<std::string> outputIdentifiers) {
+  auto inputAst = Parser::parseJson(input);
+  auto outputAst = Compiler::buildOutputBlock(outputIdentifiers);
+
+  // The input must be a block by convention, otherwise, throw an error
+  auto inputBlock = castUniquePtr<AbstractNode, Block>(std::move(inputAst));
+
+  return Compiler::compileAst(std::move(programAst), std::move(inputBlock), std::move(outputAst));
+}
diff --git a/src/parser/Parser.cpp b/src/parser/Parser.cpp
index a46293cc..039f9b72 100644
--- a/src/parser/Parser.cpp
+++ b/src/parser/Parser.cpp
@@ -26,8 +26,10 @@
 #include "ast_opt/parser/PushBackStream.h"
 #include "../../test/parser/ParserTestHelpers.h"
 #include "ast_opt/visitor/ParentSettingVisitor.h"
+#include "ast_opt/utilities/NodeUtils.h"
 
 using std::to_string;
+using json = nlohmann::json;
 
 void addParsedNode(AbstractNode *parsedNode) {
   parsedNodes.push_back(std::ref(*parsedNode));
@@ -62,6 +64,102 @@ std::unique_ptr<AbstractNode> Parser::parse(std::string s,
   return result;
 }
 
+std::unique_ptr<AbstractNode> Parser::parseJson(std::string s) {
+  return Parser::parseJson(json::parse(s));
+}
+
+std::unique_ptr<AbstractNode> Parser::parseJson(json j) {
+  // If the parsed JSON does not contain any values, return a null pointer instead of an abstract Node.
+  // There are valid cases for this, e.g., for a function without inputs, the input AST is empty.
+  if (j.empty())
+    throw stork::runtime_error("Empty abstract node encountered.");
+
+  std::string type = j["type"].get<std::string>();
+
+  if (NodeUtils::isAbstractStatement(type))
+    return Parser::parseJsonStatement(j);
+  else if (NodeUtils::isAbstractTarget(type))
+    return Parser::parseJsonTarget(j);
+  else if (NodeUtils::isAbstractExpression(type))
+    return Parser::parseJsonExpression(j);
+  else
+    throw stork::runtime_error("Unsupported type '" + type + "' in JSON object.");
+}
+
+std::unique_ptr<AbstractTarget> Parser::parseJsonTarget(json j) {
+  std::string type = j["type"].get<std::string>();
+
+  switch (NodeUtils::stringToEnum(type)) {
+    case NodeTypeVariable:
+      return Variable::fromJson(j);
+    case NodeTypeIndexAccess:
+      return IndexAccess::fromJson(j);
+    case NodeTypeFunctionParameter:
+      throw stork::runtime_error("Unsupported AbstractTarget: '" + type + "' is not yet implemented.");
+    default:
+      throw stork::runtime_error("Unsupported type: '" + type + "' is not an AbstractTarget.");
+  }
+}
+
+std::unique_ptr<AbstractExpression> Parser::parseJsonExpression(json j) {
+  std::string type = j["type"].get<std::string>();
+
+  switch (NodeUtils::stringToEnum(type)) {
+    case NodeTypeBinaryExpression:
+      return BinaryExpression::fromJson(j);
+    case NodeTypeExpressionList:
+      return ExpressionList::fromJson(j);
+    case NodeTypeLiteralBool:
+      return LiteralBool::fromJson(j);
+    case NodeTypeLiteralChar:
+      return LiteralChar::fromJson(j);
+    case NodeTypeLiteralInt:
+      return LiteralInt::fromJson(j);
+    case NodeTypeLiteralFloat:
+      return LiteralFloat::fromJson(j);
+    case NodeTypeLiteralDouble:
+      return LiteralDouble::fromJson(j);
+    case NodeTypeLiteralString:
+      return LiteralString::fromJson(j);
+    case NodeTypeOperatorExpression:
+    case NodeTypeUnaryExpression:
+    case NodeTypeCall:
+    case NodeTypeTernaryOperator:
+      throw stork::runtime_error("Unsupported AbstractExpression: '" + type + "' is not yet implemented.");
+    default:
+      // AbstractTarget is a subclass of AbstractExpression
+      try {
+        return Parser::parseJsonTarget(j);
+      }
+      catch (stork::runtime_error &) {
+        // If it's not an abstract target, then it's not an abstract expression,
+        // as we checked all other expressions before.
+        throw stork::runtime_error("Unsupported type: '" + type + "' is not an AbstractExpression.");
+      }
+  }
+}
+
+std::unique_ptr<AbstractStatement> Parser::parseJsonStatement(json j) {
+  std::string type = j["type"].get<std::string>();
+
+  switch (NodeUtils::stringToEnum(type)) {
+    case NodeTypeAssignment:
+      return Assignment::fromJson(j);
+    case NodeTypeBlock:
+      return Block::fromJson(j);
+    case NodeTypeReturn:
+      return Return::fromJson(j);
+    case NodeTypeVariableDeclaration:
+      return VariableDeclaration::fromJson(j);
+    case NodeTypeFor:
+      return For::fromJson(j);
+    case NodeTypeIf:
+      throw stork::runtime_error("Unsupported AbstractStatement: '" + type + "' is not yet implemented.");
+    default:
+      throw stork::runtime_error("Unsupported type: '" + type + "' is not an AbstractStatement.");
+  }
+}
+
 AbstractStatement *Parser::parseStatement(stork::tokens_iterator &it, bool gobbleTrailingSemicolon) {
   AbstractStatement *parsedStatement;
   if (it->isReservedToken()) {
diff --git a/src/runtime/RuntimeVisitor.cpp b/src/runtime/RuntimeVisitor.cpp
index 16b51b3e..6bf306a8 100644
--- a/src/runtime/RuntimeVisitor.cpp
+++ b/src/runtime/RuntimeVisitor.cpp
@@ -31,6 +31,10 @@ std::unique_ptr<T> castUniquePtr(std::unique_ptr<S> &&source) {
   }
 }
 
+bool SpecialRuntimeVisitor::isVariableSecretTainted(const std::string uniqueNodeId) {
+  return (secretTaintedMap.count(uniqueNodeId) > 0 && secretTaintedMap.at(uniqueNodeId));
+}
+
 std::unique_ptr<AbstractValue> SpecialRuntimeVisitor::getNextStackElement() {
   auto elem = std::move(intermedResult.top());
   intermedResult.pop();
@@ -43,10 +47,6 @@ void SpecialRuntimeVisitor::visit(BinaryExpression &elem) {
     return std::any_of(op.begin(), op.end(), [&elem](OperatorVariant op) { return elem.getOperator()==Operator(op); });
   };
   auto operatorEquals = [&elem](OperatorVariant op) -> bool { return elem.getOperator()==Operator(op); };
-  auto isSecretTainted = [&](const std::string &uniqueNodeId) -> bool {
-    // we assume here that if it is NOT in the map, then it is NOT secret tainted
-    return (secretTaintedMap.count(uniqueNodeId) > 0 && secretTaintedMap.at(uniqueNodeId));
-  };
   // ---- end
 
   elem.getLeft().accept(*this);
@@ -57,8 +57,8 @@ void SpecialRuntimeVisitor::visit(BinaryExpression &elem) {
 
   // if exactly one of the operands is a ciphertext and we have a commutative operation, then we make sure that
   // the first operand (the one we call the operation on) is the ciphertext
-  auto lhsIsSecret = isSecretTainted(elem.getLeft().getUniqueNodeId());
-  auto rhsIsSecret = isSecretTainted(elem.getRight().getUniqueNodeId());
+  auto lhsIsSecret = isVariableSecretTainted(elem.getLeft().getUniqueNodeId());
+  auto rhsIsSecret = isVariableSecretTainted(elem.getRight().getUniqueNodeId());
   if ((lhsIsSecret!=rhsIsSecret) && elem.getOperator().isCommutative()) {
     if (rhsIsSecret) std::swap(lhsOperand, rhsOperand);
   }
@@ -213,9 +213,7 @@ void SpecialRuntimeVisitor::visit(For &elem) {
     }
   };
 
-  if (elem.hasCondition()
-      && secretTaintedMap.count(elem.getCondition().getUniqueNodeId()) > 0
-      && secretTaintedMap.at(elem.getCondition().getUniqueNodeId())==true) {
+  if (elem.hasCondition() && isVariableSecretTainted(elem.getCondition().getUniqueNodeId())) {
     throw std::runtime_error("For loops over secret conditions are not supported yet!");
   }
 
@@ -269,7 +267,7 @@ void SpecialRuntimeVisitor::visit(If &elem) {
 }
 
 void SpecialRuntimeVisitor::visit(IndexAccess &elem) {
-  if (secretTaintedMap.count(elem.getUniqueNodeId()) > 0) {
+  if (isVariableSecretTainted(elem.getUniqueNodeId())) {
     throw std::runtime_error("IndexAccess for secret variables is not supported by RuntimeVisitor. "
                              "This should have already been removed by the Vectorizer. Error?");
   }
diff --git a/src/utilities/Datatype.cpp b/src/utilities/Datatype.cpp
index 9da3ef4e..fa38ac19 100644
--- a/src/utilities/Datatype.cpp
+++ b/src/utilities/Datatype.cpp
@@ -1,4 +1,5 @@
 #include <unordered_map>
+#include <ast_opt/parser/Errors.h>
 #include "ast_opt/utilities/Datatype.h"
 
 std::string enumToString(const Type type) {
@@ -14,8 +15,30 @@ std::string enumToString(const Type type) {
   return typeToString.find(type)->second;
 }
 
+Type stringToTypeEnum(const std::string s) {
+  for ( auto type : all_types) {
+    if (enumToString(type)  == s)
+      return type;
+  }
+  throw stork::runtime_error("No value type Type found for '" + s + "'!");
+}
+
+const std::string Datatype::secretPrefix = "secret ";
+
 Datatype::Datatype(Type type, bool isSecret) : type(type), isSecret(isSecret) {}
 
+Datatype::Datatype(std::string typeString) {
+  if (typeString.rfind(secretPrefix, 0) == 0) {
+    // remove secret prefix
+    this->type = stringToTypeEnum(typeString.substr(secretPrefix.size(), typeString.size() - secretPrefix.size()));
+    this->isSecret = true;
+  }
+  else {
+    this->type = stringToTypeEnum(typeString);
+    this->isSecret = false;
+  }
+}
+
 bool Datatype::operator==(const Datatype &rhs) const {
   return type==rhs.type && isSecret==rhs.isSecret;
 }
@@ -33,5 +56,5 @@ bool Datatype::getSecretFlag() const {
 }
 
 std::string Datatype::toString() const {
-  return isSecret ? "secret " + enumToString(type) : enumToString(type);
+  return isSecret ? secretPrefix + enumToString(type) : enumToString(type);
 }
diff --git a/src/utilities/NodeUtils.cpp b/src/utilities/NodeUtils.cpp
new file mode 100644
index 00000000..bf4f6c4c
--- /dev/null
+++ b/src/utilities/NodeUtils.cpp
@@ -0,0 +1,85 @@
+
+#include "ast_opt/utilities/NodeUtils.h"
+#include <unordered_map>
+
+std::string NodeUtils::enumToString(const NodeType type) {
+  std::unordered_map<NodeType, std::string> typeToString = {
+      {NodeType::NodeTypeAssignment, "Assignment"},
+      {NodeType::NodeTypeBlock, "Block"},
+      {NodeType::NodeTypeFor, "For"},
+      {NodeType::NodeTypeIf, "If"},
+      {NodeType::NodeTypeReturn, "Return"},
+      {NodeType::NodeTypeVariableDeclaration, "VariableDeclaration"},
+      {NodeType::NodeTypeFunctionParameter, "FunctionParameter"},
+      {NodeType::NodeTypeIndexAccess, "IndexAccess"},
+      {NodeType::NodeTypeVariable, "Variable"},
+      {NodeType::NodeTypeBinaryExpression, "BinaryExpression"},
+      {NodeType::NodeTypeOperatorExpression, "OperatorExpression"},
+      {NodeType::NodeTypeUnaryExpression, "UnaryExpression"},
+      {NodeType::NodeTypeCall, "Call"},
+      {NodeType::NodeTypeExpressionList, "ExpressionList"},
+      {NodeType::NodeTypeLiteralBool, "LiteralBool"},
+      {NodeType::NodeTypeLiteralChar, "LiteralChar"},
+      {NodeType::NodeTypeLiteralInt, "LiteralInt"},
+      {NodeType::NodeTypeLiteralFloat, "LiteralFloat"},
+      {NodeType::NodeTypeLiteralDouble, "LiteralDouble"},
+      {NodeType::NodeTypeLiteralString, "LiteralString"},
+      {NodeType::NodeTypeTernaryOperator, "TernaryOperator"},
+      {NodeType::NodeTypeLiteralBool, "LiteralBool"}
+  };
+  return typeToString.find(type)->second;
+}
+
+NodeType NodeUtils::stringToEnum(const std::string s) {
+  for ( int nodeTypeInt = NodeTypeAssignment; nodeTypeInt != NodeTypeLastSymbol; nodeTypeInt++ ) {
+    NodeType type = static_cast<NodeType>(nodeTypeInt);
+    if (NodeUtils::enumToString(type)  == s)
+      return type;
+  }
+  return NodeTypeLastSymbol;
+}
+
+bool NodeUtils::isAbstractStatement(const std::string s) {
+  switch (NodeUtils::stringToEnum(s)) {
+    case NodeTypeAssignment:
+    case NodeTypeBlock:
+    case NodeTypeFor:
+    case NodeTypeIf:
+    case NodeTypeReturn:
+    case NodeTypeVariableDeclaration:
+      return true;
+    default:
+      return false;
+  }
+}
+
+bool NodeUtils::isAbstractTarget(const std::string s) {
+  switch (NodeUtils::stringToEnum(s)) {
+    case NodeTypeFunctionParameter:
+    case NodeTypeIndexAccess:
+    case NodeTypeVariable:
+      return true;
+    default:
+      return false;
+  }
+}
+
+bool NodeUtils::isAbstractExpression(const std::string s) {
+  switch (NodeUtils::stringToEnum(s)) {
+    case NodeTypeBinaryExpression:
+    case NodeTypeOperatorExpression:
+    case NodeTypeUnaryExpression:
+    case NodeTypeCall:
+    case NodeTypeExpressionList:
+    case NodeTypeLiteralBool:
+    case NodeTypeLiteralChar:
+    case NodeTypeLiteralInt:
+    case NodeTypeLiteralFloat:
+    case NodeTypeLiteralDouble:
+    case NodeTypeLiteralString:
+    case NodeTypeTernaryOperator:
+      return true;
+    default:
+      return NodeUtils::isAbstractTarget(s);
+  }
+}
\ No newline at end of file
diff --git a/src/utilities/Operator.cpp b/src/utilities/Operator.cpp
index c6319cb9..315895cf 100644
--- a/src/utilities/Operator.cpp
+++ b/src/utilities/Operator.cpp
@@ -1,18 +1,16 @@
+#include <ast_opt/parser/Errors.h>
 #include "ast_opt/utilities/Operator.h"
 
 std::string toString(ArithmeticOp bop) {
-  static const std::string binaryOpStrings[] = {"+", "-", "*", "/", "%", "+++", "---", "***"};
-  return binaryOpStrings[bop];
+  return Operator::binaryOpStrings[bop];
 }
 
 std::string toString(LogicalOp logop) {
-  static const std::string logicalOpStrings[] = {"&&", "||", "<", "<=", ">", ">=", "==", "!=", "&", "^", "|"};
-  return logicalOpStrings[logop];
+  return Operator::logicalOpStrings[logop];
 }
 
 std::string toString(UnaryOp uop) {
-  static const std::string unaryOpStrings[] = {"!", "~"};
-  return unaryOpStrings[uop];
+  return Operator::unaryOpStrings[uop];
 }
 
 std::string toString(OperatorVariant opVar) {
@@ -24,6 +22,32 @@ std::string toString(OperatorVariant opVar) {
   }
 }
 
+Operator fromStringToOperatorVariant(std::string targetOpString) {
+  // XXX: this function could have a nicer implementation.
+
+  // Searches an operation (given as string) in an array opStrings of opStringSize operations.
+  // If found, it sets idx to the index of the operation in the array and returns true. Otherwise, returns false.
+  auto findOp = [targetOpString](auto &opStrings, size_t opStringSize, int &idx) {
+    for (int i{ 0 }; i < static_cast<int>(opStringSize); ++i) {
+      if (opStrings[i] == targetOpString) {
+        idx = i;
+        return true;
+      }
+    }
+    return false;
+  };
+
+  int idx;
+  if (findOp(Operator::binaryOpStrings, std::size(Operator::binaryOpStrings), idx))
+    return Operator(static_cast<ArithmeticOp>(idx));
+  if (findOp(Operator::logicalOpStrings, std::size(Operator::logicalOpStrings), idx))
+    return Operator(static_cast<LogicalOp>(idx));
+  if (findOp(Operator::unaryOpStrings, std::size(Operator::unaryOpStrings), idx))
+    return Operator(static_cast<UnaryOp>(idx));
+
+  throw stork::runtime_error("Failed to parse operator string '" + targetOpString + "' to Operator!");
+}
+
 Operator::Operator(OperatorVariant op) : op(op) {}
 
 bool Operator::isRightAssociative() const {


Test patch:
diff --git a/test/.gitignore b/test/.gitignore
new file mode 100644
index 00000000..51e3c2f8
--- /dev/null
+++ b/test/.gitignore
@@ -0,0 +1,2 @@
+# Python
+__pycache__
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
index e01286bf..5428efc3 100644
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -41,6 +41,7 @@ set(TEST_FILES
         ast/UnaryExpressionTest.cpp
         ast/VariableDeclarationTest.cpp
         ast/VariableTest.cpp
+        compiler/CompileDotProductTest.cpp
         end-to-end/BoxBlurTest.cpp
         end-to-end/CardioTest.cpp
         end-to-end/DotProductTest.cpp
@@ -69,6 +70,7 @@ set(TEST_FILES
         visitor/VisitorTest.cpp
         )
 
+
 add_executable(testing-all
         ${TEST_FILES})
 
@@ -83,8 +85,8 @@ target_link_libraries(testing-all PRIVATE gtest ast_opt gtest_main)
 target_include_directories(testing-all PRIVATE ${ast_optimizer_SOURCE_DIR}/include/)
 
 if(MSVC)
-# Mark gtest as external
-target_compile_options(testing-all PRIVATE /external:I${gtest_SOURCE_DIR}/include)
+    # Mark gtest as external
+    target_compile_options(testing-all PRIVATE /external:I${gtest_SOURCE_DIR}/include)
 endif()
 
 # create ctest targets
@@ -92,3 +94,37 @@ gtest_discover_tests(testing-all TEST_PREFIX gtest:)
 
 # make sure that ast_lib is built before the testing-all target
 add_dependencies(testing-all ast_opt)
+
+##############################
+# TARGET: testing-python
+##############################
+if(PYTHON_FRONTEND_DEV)
+    # Install pytest
+    execute_process(COMMAND ${Python3_EXECUTABLE} -m pip install --user pytest)
+
+    set(PYTEST_CMD Python3::Interpreter -m pytest --ignore=extern --ignore ${CMAKE_BINARY_DIR})
+
+    add_test(NAME PythonFrontendSimple
+            COMMAND ${PYTEST_CMD} ${PROJECT_SOURCE_DIR}/test/python_frontend/test_simple.py
+            WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/python)
+
+    add_test(NAME PythonFrontendHammingDistance
+            COMMAND ${PYTEST_CMD} ${PROJECT_SOURCE_DIR}/test/python_frontend/test_hamming_distance.py
+            WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/python)
+
+    add_test(NAME PythonMatrixVectorProduct
+            COMMAND ${PYTEST_CMD} ${PROJECT_SOURCE_DIR}/test/python_frontend/test_matrix_vector_product.py
+            WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/python)
+
+    add_test(NAME PythonSecretTainting
+            COMMAND ${PYTEST_CMD} ${PROJECT_SOURCE_DIR}/test/python_frontend/test_secret_tainting.py
+            WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/python)
+
+    add_test(NAME PythonExternalFn
+            COMMAND ${PYTEST_CMD} ${PROJECT_SOURCE_DIR}/test/python_frontend/test_external_fn.py
+            WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/python)
+
+    # make sure that python is built before the testing-all target
+    add_dependencies(testing-all python)
+endif()
+
diff --git a/test/__init__.py b/test/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/test/ast/AssignmentTest.cpp b/test/ast/AssignmentTest.cpp
index 756444b5..ea5c0819 100644
--- a/test/ast/AssignmentTest.cpp
+++ b/test/ast/AssignmentTest.cpp
@@ -1,7 +1,9 @@
 #include "ast_opt/ast/Literal.h"
 #include "ast_opt/ast/Variable.h"
 #include "ast_opt/ast/Assignment.h"
+#include "ast_opt/parser/Parser.h"
 #include "gtest/gtest.h"
+#include "../ASTComparison.h"
 
 
 TEST(AssignmentTest, values_ValuesGivenInCtorAreRetrievable) {
@@ -143,4 +145,26 @@ TEST(AssignmentTest, JsonOutputTest) { /* NOLINT */
             }
             }};
   EXPECT_EQ(assignment.toJson(), j);
+}
+
+TEST(AssignmentTest, JsonInputTest) { /* NOLINT */
+  std::string identifier = "myCustomVar";
+  int val = 2;
+  auto assignment_expected = Assignment(std::make_unique<Variable>(identifier), std::make_unique<LiteralInt>(val));
+
+  std::string assignment_json = R""""({
+    "type": "Assignment",
+    "value": {
+      "type": "LiteralInt",
+      "value": 2
+    },
+    "target": {
+      "identifier": "myCustomVar",
+      "type": "Variable"
+    }
+  })"""";
+
+  auto assignment_parsed = Parser::parseJson(assignment_json);
+
+  ASSERT_TRUE(compareAST(*assignment_parsed, assignment_expected));
 }
\ No newline at end of file
diff --git a/test/ast/BinaryExpressionTest.cpp b/test/ast/BinaryExpressionTest.cpp
index ba1fc90d..1db97f3f 100644
--- a/test/ast/BinaryExpressionTest.cpp
+++ b/test/ast/BinaryExpressionTest.cpp
@@ -1,3 +1,5 @@
+#include <ast_opt/parser/Parser.h>
+#include <test/ASTComparison.h>
 #include "ast_opt/ast/BinaryExpression.h"
 #include "ast_opt/ast/Literal.h"
 #include "gtest/gtest.h"
@@ -39,4 +41,29 @@ TEST(BinaryExpressionTest, node_iterate_children) {
 
 TEST(BinaryExpressionTest, JsonOutputTest) { /* NOLINT */
  // TODO: Verify JSON output
+}
+
+TEST(BinaryExpressionTest, JsonInputTest) { /* NOLINT */
+  BinaryExpression expected(
+      std::make_unique<LiteralInt>(1),
+      Operator(ArithmeticOp::ADDITION),
+      std::make_unique<LiteralInt>(1)
+  );
+
+  std::string binExprJson = R""""({
+    "left": {
+      "type": "LiteralInt",
+      "value": 1
+    },
+    "operator": "+",
+    "right": {
+      "type": "LiteralInt",
+      "value": 1
+    },
+    "type": "BinaryExpression"
+  })"""";
+
+  auto parsed = Parser::parseJson(binExprJson);
+
+  ASSERT_TRUE(compareAST(*parsed, expected));
 }
\ No newline at end of file
diff --git a/test/ast/BlockTest.cpp b/test/ast/BlockTest.cpp
index e8f6b107..c94fe89c 100644
--- a/test/ast/BlockTest.cpp
+++ b/test/ast/BlockTest.cpp
@@ -1,3 +1,5 @@
+#include <ast_opt/parser/Parser.h>
+#include <test/ASTComparison.h>
 #include "ast_opt/ast/Block.h"
 #include "ast_opt/ast/Literal.h"
 #include "ast_opt/ast/Variable.h"
@@ -221,4 +223,50 @@ TEST(BlockTest, JsonOutputTest) { /* NOLINT */
   };
 
   EXPECT_EQ(block.toJson(), j);
+}
+
+
+TEST(BlockTest, JsonInputTest) { /* NOLINT */
+
+  std::vector<std::unique_ptr<AbstractStatement>> statements;
+  statements.emplace_back(std::make_unique<VariableDeclaration>(
+      Datatype(Type::BOOL), std::make_unique<Variable>("foo"), std::make_unique<LiteralBool>(true)));
+  statements.emplace_back(nullptr);
+  statements.emplace_back(std::make_unique<VariableDeclaration>(
+      Datatype(Type::BOOL), std::make_unique<Variable>("boo"), std::make_unique<LiteralBool>(false)));
+  Block expected(std::move(statements));
+
+  std::string json = R""""({
+    "statements": [
+      {
+        "datatype": "bool",
+        "target": {
+          "identifier": "foo",
+          "type": "Variable"
+        },
+        "type": "VariableDeclaration",
+        "value": {
+          "type": "LiteralBool",
+          "value": true
+        }
+      },
+      {
+        "datatype": "bool",
+        "target": {
+          "identifier": "boo",
+          "type": "Variable"
+        },
+        "type": "VariableDeclaration",
+        "value": {
+          "type": "LiteralBool",
+          "value": false
+        }
+      }
+    ],
+    "type": "Block"
+  })"""";
+
+  auto parsed = Parser::parseJson(json);
+
+  ASSERT_TRUE(compareAST(*parsed, expected));
 }
\ No newline at end of file
diff --git a/test/ast/ExpressionListTest.cpp b/test/ast/ExpressionListTest.cpp
index b093b7d5..6bfcee3f 100644
--- a/test/ast/ExpressionListTest.cpp
+++ b/test/ast/ExpressionListTest.cpp
@@ -44,4 +44,8 @@ TEST(ExpressionListTest, prependStatement) {
 
 TEST(ExpressionListTest, JsonOutputTest) { /* NOLINT */
   // TODO: Json output test
+}
+
+TEST(ExpressionListTest, JsonInputTest) { /* NOLINT */
+  // TODO: Json input test
 }
\ No newline at end of file
diff --git a/test/ast/ForTest.cpp b/test/ast/ForTest.cpp
index da459e8c..a2730bc0 100644
--- a/test/ast/ForTest.cpp
+++ b/test/ast/ForTest.cpp
@@ -1,3 +1,5 @@
+#include <ast_opt/parser/Parser.h>
+#include <test/ASTComparison.h>
 #include "ast_opt/utilities/Operator.h"
 #include "ast_opt/utilities/Datatype.h"
 #include "ast_opt/ast/VariableDeclaration.h"
@@ -85,3 +87,107 @@ TEST(ForTest, node_iterate_children) { /* NOLINT */
 TEST(ForTest, JsonOutputTest) { /* NOLINT */
   // TODO: Verify JSON output
 }
+
+TEST(ForTest, JsonInputTest) {
+  auto initializer = std::make_unique<VariableDeclaration>(Datatype(Type::INT, false),
+                                                           std::make_unique<Variable>("i"),
+                                                           std::make_unique<
+                                                               LiteralInt>(0));
+  auto initializerBlock = std::make_unique<Block>(std::move(initializer));
+
+  auto condition = std::make_unique<BinaryExpression>(std::make_unique<Variable>("i"),
+                                                      Operator(LESS),
+                                                      std::make_unique<LiteralInt>(100));
+
+  auto updater = std::make_unique<Assignment>(std::make_unique<Variable>("i"),
+                                              std::make_unique<BinaryExpression>(std::make_unique<Variable>("i"),
+                                                                                 Operator(ADDITION),
+                                                                                 std::make_unique<LiteralInt>(1)));
+  auto updaterBlock = std::make_unique<Block>(std::move(updater));
+
+  auto bodyStmt = std::make_unique<VariableDeclaration>(Datatype(Type::INT, false),
+                                                        std::make_unique<Variable>("dummy"),
+                                                        std::make_unique<
+                                                            LiteralInt>(111));
+  auto bodyBlock = std::make_unique<Block>(std::move(bodyStmt));
+
+  For expected(std::move(initializerBlock),std::move(condition),std::move(updaterBlock),std::move(bodyBlock));
+
+  std::string json = R""""({
+    "body": {
+      "statements": [
+        {
+          "datatype": "int",
+          "target": {
+            "identifier": "dummy",
+            "type": "Variable"
+          },
+          "type": "VariableDeclaration",
+          "value": {
+            "type": "LiteralInt",
+            "value": 111
+          }
+        }
+      ],
+      "type": "Block"
+    },
+    "condition": {
+      "left": {
+        "identifier": "i",
+        "type": "Variable"
+      },
+      "operator": "<",
+      "right": {
+        "type": "LiteralInt",
+        "value": 100
+      },
+      "type": "BinaryExpression"
+    },
+    "initializer": {
+      "statements": [
+        {
+          "datatype": "int",
+          "target": {
+            "identifier": "i",
+            "type": "Variable"
+          },
+          "type": "VariableDeclaration",
+          "value": {
+            "type": "LiteralInt",
+            "value": 0
+          }
+        }
+      ],
+      "type": "Block"
+    },
+    "type": "For",
+    "update": {
+      "statements": [
+        {
+          "target": {
+            "identifier": "i",
+            "type": "Variable"
+          },
+          "type": "Assignment",
+          "value": {
+            "left": {
+              "identifier": "i",
+              "type": "Variable"
+            },
+            "operator": "+",
+            "right": {
+              "type": "LiteralInt",
+              "value": 1
+            },
+            "type": "BinaryExpression"
+          }
+        }
+      ],
+      "type": "Block"
+    }
+  })"""";
+
+  auto parsed = Parser::parseJson(json);
+
+  ASSERT_TRUE(compareAST(*parsed, expected));
+}
\ No newline at end of file
diff --git a/test/ast/IndexAccessTest.cpp b/test/ast/IndexAccessTest.cpp
index eb880dc3..bcabb0cc 100644
--- a/test/ast/IndexAccessTest.cpp
+++ b/test/ast/IndexAccessTest.cpp
@@ -38,4 +38,8 @@ TEST(IndexAccessTest, node_iterate_children) {
 
 TEST(IndexAccessTest, JsonOutputTest) { /* NOLINT */
  // TODO: Verify JSON output
+}
+
+TEST(IndexAccessTest, JsonInputTest) { /* NOLINT */
+// TODO: Verify JSON input
 }
\ No newline at end of file
diff --git a/test/ast/LiteralTest.cpp b/test/ast/LiteralTest.cpp
index 0419ae2f..ed1ee908 100644
--- a/test/ast/LiteralTest.cpp
+++ b/test/ast/LiteralTest.cpp
@@ -1,3 +1,5 @@
+#include <ast_opt/parser/Parser.h>
+#include <test/ASTComparison.h>
 #include "ast_opt/ast/Literal.h"
 #include "gtest/gtest.h"
 
@@ -147,6 +149,19 @@ TEST(LiteralTest, JsonOutputTestLiteralInt) { /* NOLINT */
   EXPECT_EQ(lint->toJson(), j);
 }
 
+TEST(LiteralTest, JsonInputTestLiteralInt) {
+  auto expected = LiteralInt(2);
+
+  std::string json = R""""({
+    "type": "LiteralInt",
+    "value": 2
+  })"""";
+
+  auto parsed = Parser::parseJson(json);
+
+  ASSERT_TRUE(compareAST(*parsed, expected));
+}
+
 TEST(LiteralTest, JsonOutputTestLiteralFloat) { /* NOLINT */
   float val = 33.214f;
   auto *lint = new LiteralFloat(val);
@@ -155,6 +170,19 @@ TEST(LiteralTest, JsonOutputTestLiteralFloat) { /* NOLINT */
   EXPECT_EQ(lint->toJson(), j);
 }
 
+TEST(LiteralTest, JsonInputTestLiteralFloat) {
+  auto expected = LiteralFloat(33.214f);
+
+  std::string json = R""""({
+    "type": "LiteralFloat",
+    "value": 33.214
+  })"""";
+
+  auto parsed = Parser::parseJson(json);
+
+  ASSERT_TRUE(compareAST(*parsed, expected));
+}
+
 TEST(LiteralTest, JsonOutputTestLiteralBool) { /* NOLINT */
   bool val = true;
   auto *lbool = new LiteralBool(val);
@@ -163,10 +191,38 @@ TEST(LiteralTest, JsonOutputTestLiteralBool) { /* NOLINT */
   EXPECT_EQ(lbool->toJson(), j);
 }
 
+TEST(LiteralTest, JsonInputTestLiteralBool) {
+  auto expected = LiteralBool(true);
+
+  std::string json = R""""({
+    "type": "LiteralBool",
+    "value": true
+  })"""";
+
+  auto parsed = Parser::parseJson(json);
+
+  ASSERT_TRUE(compareAST(*parsed, expected));
+}
+
 TEST(LiteralTest, JsonOutputTestLiteralString) { /* NOLINT */
   std::string val = "hello world!";
   auto *lString = new LiteralString(val);
   nlohmann::json j = {{"type", "LiteralString"},
                       {"value", val}};
   EXPECT_EQ(lString->toJson(), j);
-}
\ No newline at end of file
+}
+
+TEST(LiteralTest, JsonInputTestLiteralString) {
+  auto expected = LiteralString("hello world!");
+
+  std::string json = R""""({
+    "type": "LiteralString",
+    "value": "hello world!"
+  })"""";
+
+  auto parsed = Parser::parseJson(json);
+
+  ASSERT_TRUE(compareAST(*parsed, expected));
+}
+
+// TODO: test LiteralChar, LiteralDouble
\ No newline at end of file
diff --git a/test/ast/ReturnTest.cpp b/test/ast/ReturnTest.cpp
index a00d89b3..8ccfc996 100644
--- a/test/ast/ReturnTest.cpp
+++ b/test/ast/ReturnTest.cpp
@@ -1,3 +1,5 @@
+#include <ast_opt/parser/Parser.h>
+#include <test/ASTComparison.h>
 #include "ast_opt/ast/Literal.h"
 #include "ast_opt/ast/Variable.h"
 #include "ast_opt/ast/Return.h"
@@ -124,4 +126,21 @@ TEST(ReturnTest, JsonOutputTest) { /* NOLINT */
       }
   };
   EXPECT_EQ(r.toJson(), j);
+}
+
+
+TEST(ReturnTest, JsonInputTest) { /* NOLINT */
+  Return expected(std::make_unique<LiteralInt>(3));
+
+  std::string json = R""""({
+    "type": "Return",
+    "value": {
+      "type": "LiteralInt",
+      "value": 3
+    }
+  })"""";
+
+  auto parsed = Parser::parseJson(json);
+
+  ASSERT_TRUE(compareAST(*parsed, expected));
 }
\ No newline at end of file
diff --git a/test/ast/VariableDeclarationTest.cpp b/test/ast/VariableDeclarationTest.cpp
index 48636cb0..021494c6 100644
--- a/test/ast/VariableDeclarationTest.cpp
+++ b/test/ast/VariableDeclarationTest.cpp
@@ -1,3 +1,5 @@
+#include <ast_opt/parser/Parser.h>
+#include <test/ASTComparison.h>
 #include "ast_opt/ast/Literal.h"
 #include "ast_opt/ast/Variable.h"
 #include "ast_opt/ast/VariableDeclaration.h"
@@ -153,4 +155,30 @@ TEST(VariableDeclarationTest, JsonOutputTest) { /* NOLINT */
       }
   };
   EXPECT_EQ(var->toJson(), j);
+}
+
+TEST(VariableDeclarationTest, JsonInputTest) { /* NOLINT */
+  auto identifier = "foo";
+  auto datatype = Datatype(Type::INT);
+  int initializer = 3;
+  VariableDeclaration expected(datatype,
+                              std::make_unique<Variable>(identifier),
+                              std::make_unique<LiteralInt>(initializer));
+
+  std::string json = R""""({
+    "datatype": "int",
+    "target": {
+      "identifier": "foo",
+      "type": "Variable"
+    },
+    "type": "VariableDeclaration",
+    "value": {
+      "type": "LiteralInt",
+      "value": 3
+    }
+  })"""";
+
+  auto parsed = Parser::parseJson(json);
+
+  ASSERT_TRUE(compareAST(*parsed, expected));
 }
\ No newline at end of file
diff --git a/test/ast/VariableTest.cpp b/test/ast/VariableTest.cpp
index 838f16e8..656d804c 100644
--- a/test/ast/VariableTest.cpp
+++ b/test/ast/VariableTest.cpp
@@ -1,3 +1,5 @@
+#include <ast_opt/parser/Parser.h>
+#include <test/ASTComparison.h>
 #include "ast_opt/ast/Variable.h"
 #include "gtest/gtest.h"
 
@@ -80,3 +82,17 @@ TEST(VariableTest, JsonOutputTest) { /* NOLINT */
             {"identifier", varIdentifier}};
   EXPECT_EQ(var->toJson(), j);
 }
+
+TEST(VariableTest, JsonInputTest) {
+  std::string varIdentifier = "myVar";
+  auto var_expected = Variable(varIdentifier);
+
+  std::string var_json = R""""({
+    "identifier": "myVar",
+    "type": "Variable"
+  })"""";
+
+  auto var_parsed = Parser::parseJson(var_json);
+
+  ASSERT_TRUE(compareAST(*var_parsed, var_expected));
+}
diff --git a/test/compiler/CompileDotProductTest.cpp b/test/compiler/CompileDotProductTest.cpp
new file mode 100644
index 00000000..8ee10f4c
--- /dev/null
+++ b/test/compiler/CompileDotProductTest.cpp
@@ -0,0 +1,41 @@
+#include <vector>
+#include <stdexcept>
+#include "ast_opt/runtime/RuntimeVisitor.h"
+#include "ast_opt/utilities/Scope.h"
+#include "ast_opt/runtime/DummyCiphertextFactory.h"
+#include "ast_opt/visitor/TypeCheckingVisitor.h"
+#include "ast_opt/parser/Parser.h"
+#include "ast_opt/compiler/Compiler.h"
+#include "gtest/gtest.h"
+
+TEST(DotProductTest, pseudoCppCompile) { /* NOLINT */
+  // program's input
+  const char *inputs = R""""(
+      int x = {43,  1,   1,   1,  22, 11, 425,  0, 1, 7};
+      int y = {24, 34, 222,   4,    1, 4,   9, 22, 1, 3};
+      int size = 10;
+    )"""";
+
+  // program specification
+  const char *program = R""""(
+      int sum = 0;
+      for (int i = 0; i < size; i = i + 1) {
+          sum = sum + x[i]*y[i];
+      }
+      return sum;
+    )"""";
+
+  const std::vector<std::string> outputIdentifiers = {"sum"};
+
+  auto result = Compiler::compile(program, inputs, outputIdentifiers);
+
+  std::unordered_map<std::string, int> expectedResult;
+  expectedResult["sum"] = 5205;
+
+  for (const auto &[identifier, cipherClearText] : result) {
+    if (auto cleartextInt = dynamic_cast<Cleartext<int> *>(cipherClearText.get())) {   // result is a cleartext
+      auto cleartextData = cleartextInt->getData();
+      ASSERT_EQ(cleartextData[0], expectedResult[identifier]);
+    }
+  }
+}
\ No newline at end of file
diff --git a/test/end-to-end/DotProductTest.cpp b/test/end-to-end/DotProductTest.cpp
index 745506ba..1353306f 100644
--- a/test/end-to-end/DotProductTest.cpp
+++ b/test/end-to-end/DotProductTest.cpp
@@ -22,7 +22,7 @@ int dotProduct(const std::vector<int> &x, const std::vector<int> &y) {
   return sum;
 }
 
-TEST(DotProductTest, clearTextEvaluation) { /* NOLINT */
+TEST(CompileDotProductTest, clearTextEvaluation) { /* NOLINT */
 // program's input
   const char *inputs = R""""(
       int x = {43,  1,   1,   1,  22, 11, 425,  0, 1, 7};
diff --git a/test/python_frontend/__init__.py b/test/python_frontend/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/test/python_frontend/test_external_fn.py b/test/python_frontend/test_external_fn.py
new file mode 100755
index 00000000..ded54eb0
--- /dev/null
+++ b/test/python_frontend/test_external_fn.py
@@ -0,0 +1,48 @@
+#!/usr/bin/env python3
+
+"""
+Testing simple arithmetic FHE operations using the Python Frontend.
+"""
+
+from pyabc import *
+from .test_external_fn_helper import external_add
+
+import logging
+
+# TODO: parse this function to an AST when needed, and use function calls to it in our AST.
+
+# TODO: alternative solution: make it possible to use multiple context on the same program and only translate functions
+#   in such context to ABC ASTs and treat all others as black boxes.
+
+# def global_add(a, b):
+#     return a + b
+#
+# def test_global_add():
+#     p = ABCProgram(logging.DEBUG)
+#
+#     with ABCContext(p, logging.DEBUG):
+#         def main(a : SecretInt, b : SecretInt):
+#             a *= 3
+#             r = external_add(a, b)
+#             return r
+#
+#     r = p.execute(1, 2)
+#     assert r == 5
+#
+#     r = p.execute(-2, 5)
+#     assert r == -1
+
+def test_external_fn():
+    p = ABCProgram(logging.DEBUG)
+
+    with ABCContext(p, logging.DEBUG):
+        def main(a : NonSecretInt, b : NonSecretInt):
+            a *= 3
+            r = external_add(a, b, c = 1)
+            return r
+
+    r = p.execute(1, 2)
+    assert r == 6
+
+    r = p.execute(-2, 5)
+    assert r == 0
diff --git a/test/python_frontend/test_external_fn_helper.py b/test/python_frontend/test_external_fn_helper.py
new file mode 100644
index 00000000..d331ce09
--- /dev/null
+++ b/test/python_frontend/test_external_fn_helper.py
@@ -0,0 +1,10 @@
+
+"""
+Helper for the external function test, specifying the external function.
+"""
+
+def _internal_add2(a, b):
+    return a + b
+
+def external_add(a, b, c):
+    return _internal_add2(_internal_add2(a, b), c)
diff --git a/test/python_frontend/test_hamming_distance.py b/test/python_frontend/test_hamming_distance.py
new file mode 100755
index 00000000..bc66ebcc
--- /dev/null
+++ b/test/python_frontend/test_hamming_distance.py
@@ -0,0 +1,33 @@
+#!/usr/bin/env python3
+
+"""
+Testing the computation of the Hamming distance of two vectors using the Python Frontend for the ABC compiler.
+"""
+
+from pyabc import *
+
+import logging
+import random
+
+def test_hamming_distance():
+    p = ABCProgram(logging.DEBUG)
+
+    with ABCContext(p, logging.DEBUG):
+        def main(x : NonSecretIntVector, y : NonSecretIntVector, n : NonSecretInt):
+            sum = 0
+            for i in range(n):
+                sum += (x[i] - y[i]) * (x[i] - y[i])
+            return sum
+
+    x = [1, 1, 0, 1]
+    y = [1, 0, 1, 1]
+    n = 4
+
+    s = p.execute(x, y, n)
+    assert s == 2
+
+    n = 10
+    x = [random.randrange(100) for _ in range(n)]
+    y = [random.randrange(100) for _ in range(n)]
+    s = p.execute(x, y, n)
+    assert s == sum([(x[i] - y[i])**2 for i in range(n)])
\ No newline at end of file
diff --git a/test/python_frontend/test_matrix_vector_product.py b/test/python_frontend/test_matrix_vector_product.py
new file mode 100755
index 00000000..e5d9d563
--- /dev/null
+++ b/test/python_frontend/test_matrix_vector_product.py
@@ -0,0 +1,29 @@
+#!/usr/bin/env python3
+
+"""
+Testing a matrix vector product using FHE operations over the Python Frontend.
+"""
+
+from pyabc import *
+
+import logging
+
+def test_matrix_vector_product():
+    p = ABCProgram(logging.DEBUG)
+
+    with ABCContext(p, logging.DEBUG):
+        def main(mat : NonSecretIntVector, vec : NonSecretIntVector, m : NonSecretInt, n : NonSecretInt):
+            result = [0, 0, 0]
+            for i in range(m):
+                sum = 0
+                for j in range(n):
+                    sum += mat[i * m + j] * vec[j]
+                result[i] = sum
+            return result
+
+    n = 3
+    m = 3
+    mat = [1] * (n * m)
+    vec = [24, 34, 222]
+    r = p.execute(mat, vec, m, n)
+    assert r == [280, 280, 280]
\ No newline at end of file
diff --git a/test/python_frontend/test_secret_tainting.py b/test/python_frontend/test_secret_tainting.py
new file mode 100644
index 00000000..39f72adc
--- /dev/null
+++ b/test/python_frontend/test_secret_tainting.py
@@ -0,0 +1,45 @@
+#!/usr/bin/env python3
+
+"""
+Testing type annotations. (Currently only that the syntax compiles)
+"""
+
+from pyabc import *
+
+import logging
+
+# TODO: those test currently only test that the secret tainting syntax doesn't throw an error. However, they should
+#   check that the values marked as secret are actually treated as secrets when the AST is executed once this is implemented.
+def test_simple_secret_generic_tainting():
+    p = ABCProgram(logging.DEBUG)
+
+    with ABCContext(p, logging.DEBUG):
+        def main(a : Secret, x : NonSecret = 3):
+            # TODO: there is a bug for secret inputs: defining return variables throws the error:
+            #   RuntimeError: Initialization value of VariableDeclaration ( r) could not be processed successfully.
+            #   I suspect this is connected to secret tainting, as the result r would need to be tainted?
+            # r = a + x
+            # return r  # <-- this throws an error
+
+            a += x
+            return a
+
+    r = p.execute(1, x=2)
+    assert r == 3
+
+    r = p.execute(2)
+    assert r == 5
+
+def test_simple_secret_specific_tainting():
+    p = ABCProgram(logging.DEBUG)
+
+    with ABCContext(p, logging.DEBUG):
+        def main(a : SecretInt, x : NonSecretInt = 3):
+            a += x
+            return a
+
+    r = p.execute(1, x=2)
+    assert r == 3
+
+    r = p.execute(2)
+    assert r == 5
diff --git a/test/python_frontend/test_simple.py b/test/python_frontend/test_simple.py
new file mode 100755
index 00000000..abc19918
--- /dev/null
+++ b/test/python_frontend/test_simple.py
@@ -0,0 +1,40 @@
+#!/usr/bin/env python3
+
+"""
+Testing simple arithmetic FHE operations using the Python Frontend.
+"""
+
+from pyabc import *
+
+import logging
+
+def test_simple_arithmetic():
+    p = ABCProgram(logging.DEBUG)
+
+    with ABCContext(p, logging.DEBUG):
+        def main(a : NonSecretInt, x = 3):
+            b = (2 * 6) / x
+            y = a + b
+            return y
+
+    r = p.execute(1, x=2)
+    assert r == 7
+
+    r = p.execute(2)
+    assert r == 6
+
+def test_simple_for_loop():
+    p = ABCProgram(logging.DEBUG)
+
+    with ABCContext(p, logging.DEBUG):
+        def main(start : NonSecretInt, end : NonSecretInt, step : NonSecretInt):
+            sum = 0
+            for i in range(start, end, step):
+                sum += i
+            return sum
+
+    s = p.execute(1, 3, 1)
+    assert s == 3
+
+    s = p.execute(5, 0, -1)
+    assert s == 15