CCalc Extension

#define PY_SSIZE_T_CLEAN
#include <Python.h>

typedef enum {
    AST_LITERAL,
    AST_PLUS,
    AST_MULTIPLY,
} AstNodeType;

typedef struct _ast {
    /* The type of node this represents e.g.
       a literal, an operator etc. */
    AstNodeType type;

    /* In the case of a literal value, this field
       holds the actual value */
    double value;

    /* In the case of an operator, this pointer
       refers to the left child node */
    struct _ast *left;

    /* In the case of an operator, this pointer
       refers to the right child node */
    struct _ast *right;
} AstNode;

void
ast_print(AstNode *ast, int level)
{
    for (int i = 0; i < level; i++) {
        printf("  ");
    }

    switch(ast->type) {
    case AST_LITERAL:
        printf("%.2f\n", ast->value);
        break;

    case AST_PLUS:
        printf("+\n");
        ast_print(ast->left, level + 1);
        ast_print(ast->right, level + 1);
        break;

    case AST_MULTIPLY:
        printf("*\n");
        ast_print(ast->left, level + 1);
        ast_print(ast->right, level + 1);
        break;
    }
}

double
ast_evaluate(AstNode *ast)
{
    switch(ast->type) {
    case AST_LITERAL:
        return ast->value;

    case AST_PLUS: {
        double a = ast_evaluate(ast->left);
        double b = ast_evaluate(ast->right);

        return a + b;
    }

    case AST_MULTIPLY: {
        double a = ast_evaluate(ast->left);
        double b = ast_evaluate(ast->right);

        return a * b;
    }
    default:
        assert(0); // Unknown node type!
        return -1;
    }
}

static int
AstNode_FromPyObject(PyObject* obj, AstNode *ast, Py_ssize_t *index)
{
    PyObject* type = PyObject_GetAttrString(obj, "type");
    if (type == NULL) {
        return 0;
    }

    long node_type = PyLong_AsLong(type);
    Py_DECREF(type);

    // Get a pointer to the current node.
    AstNode *node = &ast[*index];

    switch(node_type) {
    case AST_LITERAL: {

        PyObject* value = PyObject_GetAttrString(obj, "value");
        if (value == NULL) {
            return 0;
        }

        double v = PyFloat_AsDouble(value);
        Py_DECREF(value);

        node->type = AST_LITERAL;
        node->value = v;
        node->left = NULL;
        node->right = NULL;

        // No need to recurse so return early.
        return 1;
    }
    case AST_PLUS: {
        node->type = AST_PLUS;
        break;
    }

    case AST_MULTIPLY: {
        node->type = AST_MULTIPLY;
        break;
    }

    default:
        // TODO: Raise exception
        printf("Error! Field 'type' was not a valid node type.\n");
        return 0;
    }


    PyObject *left = PyObject_GetAttrString(obj, "left");
    if (left == NULL) {
        return 0;
    }

    PyObject *right = PyObject_GetAttrString(obj, "right");
    if (right == NULL) {
        return 0;
    }

    // Increment the index to get pointers to the next array cells.
    (*index)++;
    node->left = &ast[*index];

    if (!AstNode_FromPyObject(left, ast, index)) {
        return 0;
    }

    (*index)++;
    node->right = &ast[*index];

    if (!AstNode_FromPyObject(right, ast, index)) {
        return 0;
    }

    return 1;
}

static AstNode*
AstTree_FromPyObject(PyObject* obj)
{
    // Allocate enough memory to store the resulting AST.
    Py_ssize_t num_nodes = PyObject_Length(obj);
    if (num_nodes == -1) {
        return NULL;
    }

    AstNode *ast = malloc(num_nodes * sizeof(AstNode));
    if (ast == NULL) {
        PyErr_SetString(PyExc_MemoryError, "Unable to allocate memory for the AST.") ;
        return NULL;
    }

    Py_ssize_t index = 0;
    if (!AstNode_FromPyObject(obj, ast, &index)) {
        free(ast);
        return NULL;
    }

    return ast;
}

static PyObject*
method_eval_ast(PyObject *self, PyObject *args)
{
    PyObject *obj = NULL;

    if (!PyArg_ParseTuple(args, "O", &obj)) {
        return NULL;
    }

    AstNode *ast = AstTree_FromPyObject(obj);
    if (ast == NULL) {
        return NULL;
    };

    double result = ast_evaluate(ast);
    free(ast);
    return PyFloat_FromDouble(result);
}

static PyObject*
method_hello_world(PyObject *self, PyObject *args)
{
    printf("Hello, World!\n");
    Py_RETURN_NONE;
}

static PyMethodDef ccalc_methods[] = {
    {"hello_world", method_hello_world, METH_VARARGS, "Print 'Hello, World!'."},
    {"eval_ast", method_eval_ast, METH_VARARGS, "Evaluate the given ast."},
    {NULL, NULL, 0, NULL}
};

static struct PyModuleDef ccalcmodule = {
    PyModuleDef_HEAD_INIT,
    "_ccalc",
    "Simple calculator implemented in C",
    -1,
    ccalc_methods
};

PyMODINIT_FUNC
PyInit__ccalc()
{
    return PyModule_Create(&ccalcmodule);
}
build
dist
__pycache__
*.pyc
*.egg-info
import _ccalc

eval_ast = _ccalc.eval_ast

class AstNode:
    """Base AstNode class."""

    LITERAL = 0
    PLUS = 1
    MULTIPLY = 2

    def __init__(self, type=None, value=None, left=None, right=None):

        if left is not None and not isinstance(left, AstNode):
            left = Literal(left)

        if right is not None and not isinstance(right, AstNode):
            right = Literal(right)

        self.type = type
        self.value = value
        self.left = left
        self.right = right

    def __repr__(self):

        name = self.__class__.__name__
        detail = ""

        if self.value is not None:
            detail = str(self.value)
        else:
            detail = f"{self.left}, {self.right}"

        return f"{name}<{detail}>"

    def __len__(self):
        left = 0 if self.left is None else len(self.left)
        right = 0 if self.right is None else len(self.right)

        return 1 + left + right

    def __add__(self, other):
        return Plus(self, other)

    def __radd__(self, other):
        return Plus(other, self)

    def __mul__(self, other):
        return Multiply(self, other)

    def __rmul__(self, other):
        return Multiply(other, self)


class Literal(AstNode):

    def __init__(self, value):
        if not isinstance(value, (int, float)):
            message = "Type '{}' is not a valid literal"
            raise TypeError(message.format(type(value)))

        super().__init__(type=AstNode.LITERAL, value=float(value))


class Plus(AstNode):

    def __init__(self, left, right):
        super().__init__(type=AstNode.PLUS, left=left, right=right)


class Multiply(AstNode):

    def __init__(self, left, right):
        super().__init__(type=AstNode.MULTIPLY, left=left, right=right)
from setuptools import setup, Extension

ccalcmod = Extension("_ccalc", sources=["ccalcmodule.c"], language="c")

setup(
    name="ccalc",
    version="1.0.0",
    description="A simple calculator implemented in C",
    packages=["ccalc"],
    ext_modules=[ccalcmod],
)
A CPython extension that embeds the ``simple-ast`` program into Python.

.. code-block:: python

   >>> import ccalc
   >>> expression = (ccalc.Literal(1) + 2) * 3
   >>> expression
   Multiply<Plus<Literal<1.0>, Literal<2.0>>, Literal<3.0>>

   >>> ccalc.eval_ast(expression)
   9.0

Building
--------

It's probably worth creating a virtual environment to work in

.. code-block:: console

   $ python -m venv .env

Assuming you have a C compiler available, building the extension is as easy as
running the following command

.. code-block:: console

   (.env) $ python setup.py install 

README

A CPython extension that embeds the simple-ast program into Python.

>>> import ccalc
>>> expression = (ccalc.Literal(1) + 2) * 3
>>> expression
Multiply<Plus<Literal<1.0>, Literal<2.0>>, Literal<3.0>>

>>> ccalc.eval_ast(expression)
9.0

Building

It’s probably worth creating a virtual environment to work in

$ python -m venv .env

Assuming you have a C compiler available, building the extension is as easy as running the following command

(.env) $ python setup.py install