Foundation¶
Hercules is designed to write python code that is so easy to convert to other C/C++ languages which are more efficient and faster than python. The main goal is to write python code that is easy to convert to C/C++ code. So, if you are familiar with python, you already be a master of 95% of Hercules.
In this section, all the code examples are written in jupyter notebook. You can find the jupyter notebook in the examples/tutorial/foundations.ipynb file.
Print¶
print("Hello, World!")
from sys import stderr
print('hello world', end='', file=stderr)
Literals¶
# Booleans
True # type: bool
False
# Numbers
a = 1 # type: int; a signed 64-bit integer
b = 1.12 # type: float; a 64-bit float (just like "double" in C)
c = 5u # unsigned int; an unsigned 64-bit int
d = Int[8](12) # 8-bit signed integer; you can go all the way to Int[2048]
e = UInt[8](200) # 8-bit unsigned integer
f = byte(3) # Hercules's byte is equivalent to C's char; equivalent to Int[8]
h = 0x12AF # hexadecimal integers are also welcome
g = 3.11e+9 # scientific notation is also supported
g = .223 # and this is also float
g = .11E-1 # and this as well
# Strings
s = 'hello! "^_^" ' # type: str
t = "hello there! \t \\ '^_^' " # \t is a tab character; \\ stands for \
raw = r"hello\n" # raw strings do not escape slashes; this would print "hello\n"
fstr = f"a is {a + 1}" # an f-string; prints "a is 2"
fstr = f"hi! {a+1=}" # an f-string; prints "hi! a+1=2"
t = """
hello!
multiline string
"""
# The following escape sequences are supported:
# \\, \', \", \a, \b, \f, \n, \r, \t, \v,
# \xHHH (HHH is hex code), \OOO (OOO is octal code)
Assignments and operators¶
a = 1 + 2 # this is 3
a = (1).__add__(2) # you can use a function call instead of an operator; this is also 3
a = int.__add__(1, 2) # this is equivalent to the previous line
b = 5 / 2.0 # this is 2.5
c = 5 // 2 # this is 2; // is an integer division
a *= 2 # a is now 6
Each operator has a corresponding magic method that can be called explicitly. For example, + is __add__, - is __sub__.below is the list of binary operators and each one’s associated magic method:
Operator |
Magic Method |
|---|---|
+ |
__add__ |
- |
__sub__ |
* |
__mul__ |
/ |
__truediv__ |
// |
__floordiv__ |
% |
__mod__ |
** |
__pow__ |
@ |
__matmul__ |
<< |
__lshift__ |
>> |
__rshift__ |
& |
__and__ |
| |
__or__ |
^ |
__xor__ |
<< |
__lshift__ |
>> |
__rshift__ |
< |
__lt__ |
<= |
__le__ |
== |
__eq__ |
!= |
__ne__ |
in |
__contains__ |
and |
n/a |
or |
n/a |
there are also unary operators, expressions like b = +a, -a.
Operator |
Magic Method |
Description |
|---|---|---|
+ |
__pos__ |
unary positive |
- |
__neg__ |
unary negative |
~ |
__invert__ |
bitwise NOT |
Control flow¶
If statements¶
common if statement like in python:
d = 2
if d > 3:
print("d is greater than 3")
elif d == 3:
print("d is equal to 3")
else:
print("d is less than 3")
extended c++’s switch statement to be a match statement:
def random_number():
import random
return random.randint(0, 20)
def match_ex():
for _ in range(20):
match random_number(): # assuming that the type of this expression is int
case 1: # is it 1?
print('hi')
case 2 ... 10: # is it 2, 3, 4, 5, 6, 7, 8, 9 or 10?
print('wow!')
case _: # "default" case
print('meh...')
match_ex()
The match statement is a powerful tool that can be used to replace if-elif-else statements. It can be used to
match bool_expr(): # now it's a bool expression
case True:
print('yay')
case False:
print('nay')
The match statement can also be used to match against other types of expressions:
match str_expr(): # now it's a str expression
case 'abc': print("it's ABC time!")
case 'def' | 'ghi': # you can chain multiple rules with the "|" operator
print("it's not ABC time!")
case s if len(s) > 10: print("so looong!") # conditional match expression
case _: assert False
match some_tuple: # assuming type of some_tuple is Tuple[int, int]
case (1, 2): ...
case (a, _) if a == 42: # you can do away with useless terms with an underscore
print('hitchhiker!')
case (a, 50 ... 100) | (10 ... 20, b): # you can nest match expressions
print('complex!')
list matching:
match list_foo():
case []: # [] matches an empty list
print('A')
case [1, 2, 3]: # make sure that list_foo() returns List[int] though!
print('B')
case [1, 2, ..., 5]: # matches any list that starts with 1 and 2 and ends with 5
print('C')
case [..., 6] | [6, ...]: # matches a list that starts or ends with 6
print('D')
case [..., w] if w < 0: # matches a list that ends with a negative integer
print('E')
case [...]: # any other list
print('F')
Loops¶
hercules supports the standard python loops like for and while loops.
for loop, for construct an iterator over any generator, which means any object that has a __iter__ method. In Practice, this means that generators, lists, tuples, sets, and homogenous tuples, ranges, and many more types implement this method. If you need to implement one yourself, just keep in mind that __iter__ is a generator and not a function.
for i in range(5):
print(i)
for i in range(1, 10, 2): # start, stop, step
print(i)
for i in range(10, 1, -1): # start, stop, step
print(i)
for i in [1, 2, 3, 4, 5]:
print(i)
for i in 'hello':
print(i)
for i in range(5):
if i == 3:
break
print(i)
else:
print('no break')
while loop:
i = 0
while i < 5:
print(i)
i += 1
i = 0
while True:
if i == 5:
break
print(i)
i += 1
i = 0
while i < 5:
i += 1
if i == 3:
continue
print(i)
Imports¶
The import statement is used to import modules. The from statement is used to import specific functions or classes from a module.
like below, importing the system module math and using the sqrt function:
import math
print(math.sqrt(4))
from math import sqrt
print(sqrt(4))
importing a module with an alias:
import math as m
print(m.sqrt(4))
from math import sqrt as s
print(s(4))
using the from statement to import all functions from a module:
from math import *
print(sqrt(4))
from math import sqrt, sin, cos
print(sqrt(4))
print(sin(0))
print(cos(0))
from math import sqrt as s, sin as si, cos as c
print(s(4))
print(si(0))
print(c(0))
importing a C function. The C function is a function that is written in C and can be used in Hercules, all the C functions are defined in the C module.
Assuming that you defined a function print_hello in a file called print_hello.c:
#include <stdio.h>
void print_hello() {
printf("Hello, World!\n");
}
and you compiled it to a shared library called print_hello.so:
gcc -shared -o print_hello.so print_hello.c
then you can import and use it in Hercules like this:
from C import print_hello
print_hello()
then import some_module looks for a file called some_module.hs in the current directory and stdlib directories, or looks for a directory called some_module and tries to import some_module/__init__.hs.
Note
The C module is a special module that is used to import C functions. But it does not specify the path of the dynamic library and the library should be loaded in the current app context. Next mailstone design to specify the name of the dynamic library and the path of the dynamic library, and load the dynamic library in the current app context. it is assumed like this:
from C(path=’path/to/dynamic/library’, name=’dynamic_library_name’) import function_name.`
the path can is optional and if it is not specified, and can be relative to the rpath of the current app context, or the absolute path.
the name is optional and if it is not specified, it is assumed to load the function from the dynamic libraries that has loaded in the current app context.
Exceptions¶
hercules supports the standard python exceptions like try, except, finally and raise. below is an example of how to use them:
def throwable():
raise ValueError("doom and gloom")
try:
throwable()
except ValueError as e:
print("we caught the exception")
except:
print("ouch, we're in deep trouble")
finally:
print("whatever, it's done")
Warning
Right now, Hercules cannot catch multiple exceptions in one statement. Thus catch (Exc1, Exc2, Exc3) as var will not compile, since the type of var needs to be known ahead of time.
Context life cycle¶
If you have an object that implements __enter__ and __exit__ methods to manage its lifetime (say, a File), you can use a with statement to make your life easier:
with open('file.txt', 'w') as f:
f.write('hello, world!')
# f is closed here
Statics¶
Sometimes, certain values or conditions need to be known at compile time. For example, the bit width N of an integer type Int[N], or the size M of a static array __array__[int](M) need to be compile time constants.
To accomodate this, Hercules uses static values, i.e. values that are known and can be operated on at compile time. Static[T] represents a static value of type T. Currently, T can only be int or str.
For example, we can parameterize the bit width of an integer type as follows:
N: Static[int] = 32
a = Int[N](10) # 32-bit integer 10
b = Int[2 * N](20) # 64-bit integer 20
All of the standard arithmetic operations can be applied to static integers to produce new static integers.
Statics can also be passed to the compiler via the -D flag, as in -DN=32.
Classes can also be parameterized by statics:
class MyInt[N: Static[int]]:
n: Int[N]
x = MyInt[16](i16(42))
this equivalent to the following code in C++:
template<int N>
class MyInt {
public:
int n[N];
};
MyInt<16> x;
Static evaluation¶
In certain cases a program might need to check a particular type and perform different actions based on it. For example:
def flatten(x):
if isinstance(x, list):
for a in x:
flatten(a)
else:
print(x)
flatten([[1,2,3], [], [4, 5], [6]])
Standard static typing on this program would be problematic since, if x is an int, it would not be iterable and hence would produce an error on for a in x. we solves this problem by evaluating certain conditions at compile time, such as isinstance(x, list), and avoiding type checking blocks that it knows will never be reached. In fact, this program works and flattens the argument list.
Static evaluation works with plain static types as well as general types used in conjunction with type, isinstance or hasattr.
this may like c++11’s type traits, but it is more powerful and can be used in more cases. like the following example:
template<typename T>
struct is_vector : std::false_type {};
template<typename T>
struct is_vector<std::vector<T>> : std::true_type {};
template<typename T>
void some_function( const T& t ) {
if constexpr( is_vector<T>::value ) {
// do something with a vector
} else {
// do something with a non-vector
}
}
Note
The if constexpr statement is a C++17 feature that allows you to conditionally compile code based on a compile-time constant. It is similar to the if statement, but the condition is evaluated at compile time, and the branch that is not taken is discarded from the compiled binary.
Note
The C++17 if constexpr statement and hercules’s static evaluation are similar, but the hercules’s static evaluation is more powerful and can be used in more cases. They all inspired by the functional programming languages like erlang’s pattern matching. C++ and hercules’s static evaluation are more efficient than python’s solution, because they are evaluated at compile time, and provide a chance to optimize the code in IR pass.
Comments¶