This is the Mamba programming language. The Documentation can be found here. This documentation outlines the different language features, and also contains a formal specification of the language.
In short, Mamba is like Python, but with a few key features:
This is a transpiler, written in Rust, which converts Mamba source files to Python source files. Mamba code should therefore be interoperable with Python code. Functions written in Python can be called in Mamba and vice versa (from the generated Python files).
Below are some code examples to showcase the features of Mamba. We highlight how functions work, how de define classes, how types and type refinement features are applied, how Mamba can be used to ensure pureness, and how error handling works.
We can write a simple script that computes the factorial of a value given by the user.
```mamba def factorial(x: Int) -> Int => match x 0 => 1 n => n * factorial(n - 1)
def num := input("Compute factorial: ") if num.is_digit() then def result := factorial(int(num)) print("Factorial {num} is: {result}.") else print("Input was not an integer.") ```
Notice how here we specify the type of argument x
, in this case an Int
, by writing x: Int
. This means that the
compiler will check for us that factorial is only used with integers as argument.
Classes are similar to classes in Python, though we can for each function state whether we can write to self
or not by
stating whether it is mutable or not. If we write self
, it is mutable, whereas if we write fin self
, it is immutable
and we cannot change its fields. We can do the same for any field. We showcase this using a simple dummy Server
object.
```mamba import ipaddress
class ServerError(def message: String): Exception(message)
def fin alwaysthesame_message := "Connected!"
class MyServer(def ipaddress: IPv4Address) def isconnected: Bool := False def lastmessage: String := None
def last_sent(fin self) -> String raise ServerError => if self._last_message /= None
then self._last_message
else raise ServerError("No last message!")
def connect(self) =>
self.is_connected := true
print(always_the_same_message)
def send(self, message: String) raise ServerError => if self.is_connected
then self._last_message := message
else raise ServerError("Not connected!")
def disconnect(self) => self.is_connected := False
```
Notice how:
self
is not mutable in last_sent
, meaning we can only read variables, whereas in connect self
is mutable, so we
can change properties of `selWhich we can then use as follows in our script:
```mamba import ipaddress from server import MyServer
def fin someip := ipaddress.ipaddress("151.101.193.140") def myserver := MyServer(someip)
httpserver.connect() if myserver.isconnected then httpserver.send("Hello World!")
print("last message sent before disconnect: \"{myserver.lastsent()}\".") my_server.disconnect() ```
As shown above Mamba has a type system. Mamba however also has type refinement features to assign additional properties to types.
Lets expand our server example from above, and rewrite it slightly:
```mamba import ipaddress
type Server def ip_address: IPv4Address
def connect() -> () raise ServerErr
def send(String) -> () raise ServerErr
def disconnect() -> ()
type ConnectedMyServer: MyServer when self.isconnected type DisconnectedMyServer: MyServer when not self.isconnected
class ServerErr(def message: String): Exception(message)
class MyServer(self: DisconnectedMyServer, def ipaddress: IPv4Address): Server def isconnected: Bool := False def lastmessage: String := None
def last_sent(self) -> String raise ServerErr => if self.last_message /= None
then self._last_message
else raise ServerError("No last message!")
def connect(self: DisconnectedMyServer) => self.is_connected := True
def send(self: ConnectedMyServer, message: String) => self._last_message := message
def disconnect(self: ConnectedMyServer) => self.is_connected := False
```
Notice how above, we define the type of self
.
Each type effectively denotes another state that self
can be in. For each type, we use when
to show that it is a
type refinement, which certain conditions.
```mamba import ipaddress from server import MyServer
def fin someip := ipaddress.ipaddress("151.101.193.140") def myserver := MyServer(someip)
http_server.connect()
if myserver isa ConnectedMyServer then # httpserver is a Connected Server if the above is true my_server.send("Hello World!")
print("last message sent before disconnect: \"{myserver.lastsent}\".") if myserver isa ConnectedMyServer then myserver.disconnect() ```
Type refinement also allows us to specify the domain and co-domain of a function, say, one that only takes and returns positive integers:
```mamba type PositiveInt: Int when self >= 0 else "Must be greater than 0"
def factorial(x: PositiveInt) -> PositiveInt => match x 0 => 1 n => n * factorial(n - 1) ```
In short, types allow us to specify the domain and co-domain of functions with regards to the type of input, say, Int
or String
. During execution, a check is done to verify that the variable does conform to the requirements of the
refined type. If it does not, an exception is raised.
Type refinement allows us to to some additional things:
Mamba has features to ensure that functions are pure, meaning that if x = y
, for any f
, f(x) = f(y)
(except if the
output of the function is say None
or NaN
). By default, functions are not pure, and can read any variable they want,
such as in Python. When we make a function pure
, it cannot:
self
. Mainly since self
is never given as an argument, so a function output only
depends on its explicit arguments.When a function is pure
, its output is always the same for a given input. When a variable is immutable, when we
add fin
, it can never change. So, pure
is a property of functions, and fin
is a property of variables.
```mamba
def fin taylor := 7
def pure sin(x: Int) => def ans := x for i in 1 ..= taylor step 2 do ans := (x ^ (i + 2)) / (factorial (i + 2)) ans ```
Generally speaking, global variables can cause a lot of headaches. Immutable variables and pure functions make it easy to write declarative programs with no hidden dependencies.
Unlike Python, Mamba does not have try
except
and finally
(or try
catch
as it is sometimes known). Instead, we
aim to directly handle errors on-site so the origin of errors is more tracable. The following is only a brief example.
Error handling can at times becomes quite verbose, so we do recommend checking out
the docs on error handling to get a better feel
for error handling.
We can modify the above script such that we don't check whether the server is connected or not. In that case, we must
handle the case where my_server
throws a ServerErr
:
```mamba import ipaddress from server import MyServer
def fin someip := ipaddress.ipaddress("151.101.193.140") def myserver := MyServer(someip)
def message := "Hello World!" my_server.send(message) handle err: ServerErr => print("Error while sending message: \"{message}\": {err}")
if myserver isa ConnectedMyServer then myserver.disconnect() ```
In the above script, we will always print the error since we forgot to actually connect to the server. Here we showcase
how we try to handle errors on-site instead of in a (large) try
block. This means that we don't need a finally
block: We aim to deal with the error where it happens and then continue executing the remaining code. This also prevents
us from wrapping large code blocks in a try
, where it might not be clear what statement or expression might throw what
error.
handle
can also be combined with an assign. In that case, we must either always return (halting execution or exiting
the function), or evaluate to a value. This is shown below:
```mamba def a := functionmaythrow_err() handle err: MyErr => print("We have a problem: {err.message}.") return # we return, halting execution err: MyOtherErr => print("We have another problem: {err.message}.") 0 # ... or we assign default value 0 to a
print("a has value {a}.") ```
If we don't want to use a handle
, we can simply use raise
after a statement or exception to show that its execution
might result in an exception, but we don't want to handle that here. See the sections above for examples where we don't
handle errors and simply pass them on using raise
.
The transpiler can be split up into three distinct stages: parsing of the input, checking the input, and converting the input to Python code.
Convert a string of characters to a string of tokens. For each token we store the starting position within the file and its width. This information is used when generating error messages in this and consecutive stages.
During this stage, errors are raised if we encounter an illegal character. We then convert the list of tokens to an Abstract Syntax Tree (AST) based on the pre-defined grammar of the language.
During this stage syntax errors are raised if we encounter an illegal strings of tokens. I.e. a list of tokens that does not conform to the grammar of the language. A lex error is thrown if something goes wrong during the lexing stage.
Check that the AST is correctly formed, beyond what the parser can check. We also verify that we only call methods which are members of an object's class and that functions are used properly, which are imported. We do mutability checks, that we only call variables which are initalized, and that functions receive only types they expect (at the very least an expression which may be evaluated). More complex language features are also type checked.
This stage generates type errors, and contains the bulk of applicationl logic. This is also the last stage where error messages may be generated. Any error after this stage is indicative of an internal error, and should be fixed.
Note that we do type checking before desugaring to improve the quality of error messages. This is the same approach Haskell uses, at the cost of increased complexity of the type checker.
Convert the AST to a simpler core language which looks similar to Python. This internal language is then again converted to a string which represents Python code.
Note that in future, the type checker should annotate the tree such that each node has a type. This will allow us to add type hints to all Python code we output, and to perhaps also more easily desugar more complex language construts (such as classes).
mamba [FLAGS] [OPTIONS]
-d, --debug Add line numbers to log statements
-h, --help Prints help information
-l, --level Print log level
--no-module-path Disable the module path in the log statements
--no-color Disable colorized output
-v Set level of verbosity
- : info, error, warning printed to sterr (Default)
- v : debug messages are printed
- vv : trace messages are printed
-V, --version Prints version information
-i, --input <INPUT> Input file or directory.
If file, file taken as input.
If directory, recursively search all sub-directories for *.mamba files.
If no input given, current directory used as input directory.
-o, --output <OUTPUT> Output directory to store Python files.
Output directory structure reflects input directory structure.
If no output given, 'target' directory created in current directory and is used as ouput.
You can type mamba -help
for a message containing roughly the above information.
Before submitting your first issue or pull request, please take the time to read both our contribution guidelines and our code of conduct.