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);
}
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
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],
)
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)
build dist __pycache__ *.pyc *.egg-info
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