thanks to aphitorite for the beautiful logo!
opal is a transcompiled programming language based on Python and Cython.
[ ] = optional
- To compile to a Python
.py
file:opal pycompile input_file [output_file]
- To compile to a Cython
.pyx
file:opal pyxcompile input_file [output_file]
- To compile to a Cython module:
opal compile input_file [output_file]
- To compile to an executable:
opal release build_file
oropal pyrelease build_file
(if the program is--nocompile
). Note that this requires an internet connection to fetch and install dependencies to the target. An opal build file is structured like a ianthe project file; - To directly run opal source:
opal file_name
--type-mode
- Selects a default typing mode for the file. Options are:
hybrid
: The default one. Forces the type when it doesn't create problems, checks otherwise;check
: Checks types. Some conversions won't be automatic (for example, assigning atuple
to alist
typed variable won't convert it automatically);force
: Always forces types. Can break programs as forced typing can't always be performed;none
: Uses dynamic typing for all variables.
- Usage: --type-mode mode
- Selects a default typing mode for the file. Options are:
--disable-notes
- Disables notes during compilation
- Usage: --disable-notes
--dir
- Specifies a custom
HOME_DIR
variable. - Usage: --dir path
- Specifies a custom
--static
- Treats every variable as it cannot change types. Useful for optimization purposes.
- Usage: --static
--nostatic
- Specifies that a program cannot be compiled with the
--static
flag. It's not meant to be used via terminal.
- Specifies that a program cannot be compiled with the
--nocompile
- Specifies that a program cannot be compiled. Useful for programs that use Python features that are not included in Cython. It's not meant to be used via terminal.
--require
- Specifies a minimum version of opal that the program requires to run. It's not meant to be used via terminal.
--compile-only
- Specifies that a program can only be compiled. Useful for programs that use Cython instructions or features. It's not meant to be used via terminal.
--module
- Specifies that a program is a module. It's not meant to be used via terminal.
--debug
- Saves the Cython annotations file when compiling for debugging purposes.
- Usage: --debug
To properly run opal code, you will need to install the Python modules listed in requirements.txt.
opal only supports Python 3.10 and higher.
Once files are downloaded on your machine, run opalc.py build
to build the standard library and the runner executable.
package opal: import *;
main {
IO.out("Hello World!\n");
}
The new
statement uses the following syntax:
new itemType itemName
new
will accept as types:
- Python integrated types;
- Custom defined types;
dynamic
- opal specific keywords.
Example:
# this defines the variable and assigns to it
new int aNumber = 2;
# this only defines the variable
new str name;
# var can accept any type
new dynamic var;
Creates a function. Example:
new function functionName(arg0, arg1) {
# your code here
}
The types of a function's parameter can be specified...
new function functionName(arg0: int, arg1: str) {
# your code here
}
... default arguments can be defined...
new function functionName(arg0: int = 2 + 2, arg1 = "hi") {
# your code here
}
... and return types can be specified.
new function add(a: int, b: int) int {
return a + b;
}
Creates a class. Example:
new class ClassName {
# your code here
}
Classes can also inherit content from one or more parent classes:
new class ClassName : ParentClassA, ParentClassB {
# your code here
}
Creates a method and passes it the this
variable, which refers to the class the method is in. this
is equivalent to Python's self
. The syntax is the same as a normal function.
Creates a method with a @staticmethod
decorator. The syntax is the same as a normal function.
Creates a method with a @classmethod
decorator, and passes it the this
variable. The syntax is the same as a normal function.
Creates a basic class containing the specified properties. Example:
new record RecordName(arg0, arg1, arg2);
Records can also inherit from classes, using the following syntax:
new class ParentClass {}
new record RecordName(arg0, arg1, arg2) <- ParentClass;
if
statements are equivalent to Python's:
if someCondition {
# your code here
} elif someOtherCondition {
# some other code here
} else {
# do something else
}
match
statements have two implementations. The default one is equivalent to Python's match
statement:
match aVariable {
case aValue {
# aVariable == aValue
}
case 2 {
# aVariable == 2
}
default {
# aVariable is not in any of the cases
# the default statement should always be last
# (or second to last in case a
# found statement is used)
}
found {
# this code will execute if any of the cases is met
# the found statement should always be last
}
}
The other match
implementation consists in an elif
chain. It's accessible by specifying the operator to be used.
match:(!=) aVariable {
# cases here
}
if no operator is specified (match:()
), ==
will be used by default.
NOTE: since Cython doesn't support Python's match
statement, opal will always fall back to the elif
chain implementation when compiling.
while someCondition {
# your code here
}
do
statements implement a do-while loop:
do {
# your code here
# the condition will be checked at the end
# of each iteration
} while someCondition;
This syntax is also valid and equivalent:
do someCondition {
# your code here
}
Repeats code a certain amount of times. Can be either a constant or a variable.
repeat times {
# repeating code
}
for
loops can use either Python syntax...
for item in list {
# do something
}
for i in range(0, 10) {
# do something else
}
... or a C-like syntax:
for i = 0; i < 10; i++ {
# do something
}
Variables in for loops don't have to be defined separately.
Any of these loops can use break
and continue
statements.
opal's exception handling follows Python's syntax, with different keywords.
try {
# something that might give an error
}
# if a ValueError occurs, do nothing
ignore ValueError;
catch Exception as e {
# something went wrong
} success {
# no error occurred
# you can also use the "else" keyword instead of "success"
}
To throw exceptions, you can use the throw
statement, which is equivalent to Python's raise
.
import aModule;
package anotherModule: import item0 as myItem, item1;
# this is equivalent to:
# from anotherModule import item0 as myItem, item1
package aModule: import *;
# this is equivalent to:
# from aModule import *
Class methods can be made abstract. if a class contains an abstract method, the class must be declared abstract as well:
abstract: new class AnAbstractClass {
new method add(a, b) {
return a + b;
}
abstract: new method anAbstractMethod();
}
You can create get-set methods for properties by creating a property
:
new class MyClass {
new method __init__(a) {
this.a = a;
}
property myProperty {
get {
return this.a;
}
set {
this.a = value;
# the "value" variable name can be changed:
# set(myValue) {
# this.a = myValue;
# }
}
delete {
del this.a;
}
}
}
Any of the methods can be omitted. For example, if the set
method is not defined, the property will be read-only. Property methods can be set as abstract. A property method can also be defined outside of a property
statement:
new class myClass {
new method __init__(a) {
this.a = a;
}
property myProperty {
get {
return this.a;
}
delete {
del this.a;
}
}
set<myProperty>(myValue) {
this.a = myValue;
}
}
The main
statement acts as syntax sugar for the form if __name__ == "__main__":
.
main {
# this code will only be executed if the script
# is not imported
}
main() {
# using brackets generates a main function.
# defining one is not mandatory, but it's
# generally good practice. only one main
# function can be defined
}
Creates an identifier. This is necessary as every identifier mentioned at the beginning of an expression statement must be known at compile-time. For example:
new function a(x) {
b(x - 1);
}
new function b(x) {
if x < 2 {
a(x);
}
}
Here, the compiler will give us an error:
error (in test.opal: a(), line 2, pos 4): unknown statement or identifier
1 | new function a(x) {
2 | b(x - 1);
| ^
3 | }
4 |
5 | new function b(x) {
To solve this, we can create the identifier ourselves:
use b;
new function a(x) {
b(x - 1);
}
new function b(x) {
if x < 2 {
a(x);
}
}
Creates a namespace. Effectively just a class that can't inherit from other classes and can't be instantiated.
namespace MyNamespace {
# your code here
}
Creates a set of variables contaning distinct constants:
enum MyEnum {
CONST0, CONST1, CONST2
}
# MyEnum.CONST0 == 0
# MyEnum.CONST1 == 1
# ...
The values of each constant can be chosen when defining the enum
:
enum MyEnum {
CONST0 = "hi",
CONST1 = 2,
CONST2 = 3.14
}
enum
s can also be defined with no name. In that case, the constants get created as actual variables hidden behind no namespace:
enum {
CONST0, CONST1, CONST2
}
# you can now access the variables directly
# for example:
new int myVariable = CONST0 + CONST1;
The unchecked
flag is used to ignore typing on an assignment or skip checks on other statements. Statements to which the unchecked
flag can be applied are:
repeat
: skips the conversion to an absolute int;return
: ignores type checking. Example:
new int a = 2 + 2;
unchecked: a += 2;
unchecked: repeat a {
# your code here
}
new function add(a: int, b: str | int) int {
if type(b) is int {
unchecked: return a + b;
}
return a + int(b);
}
The static
flag is used to indicate whether a variable or a block of variables will not change type. This is used to apply optimizations during compilation. Example:
static {
new int a;
new float b = 2.0;
}
static: new int c = 3;
static:
new function myFunction() {
# every variable here will be static
}
static:
namespace Test {
# every variable here will be static
}
The inline
flag tries to inline an optimizable function during compilation. The compiler will throw an error if the function to be inlined is not optimizable.
inline:
new function add(a: int, b: int) int {
return a + b;
}
The global
flag is used to declare an object in the global scope.
new function aFunction() {
global: new int a = 2, b = 3;
global:
new function anotherFunction() {
# do something
}
}
# a, b, and anotherFunction will be visible here with a "use" statement
Some statements are direct equivalents of Python statements or functions. Here's a list of opal statements that haven't been mentioned yet and their Python equivalents:
opal | Python
--------------------
async | async
await | await
with | with
super | super()
del | del
assert | assert
yield | yield
global | global
external | nonlocal
opal supports decorators, using the same syntax as Python:
@myDecorator;
new function myFunction() {}
Comments are marked with the #
symbol and extend until a newline is found.
Defines a constant.
$define constantName constantContent
Defines a constant that is only visible to the precompiler.
$pdefine constantName constantContent
opal will automatically create some "pconstants" you can use:
HOME_DIR
: points to the base directory of the given file;CY_COMPILING
: abool
that indicates whether the compiler is compiling in Cython mode;RELEASE_MODE
: abool
that indicates whether the compiler is compiling in release mode;TARGET_FILE
: astr
that points to the file opal is generating. It'sNone
when opal is directly running code.
Includes a Python or opal file inside an opal file. Expects a str
or path-like argument (it gets evaluated using Python's eval
). Usage of the os
module is allowed and recommended, especially to join directories and filenames.
$include os.path.join(HOME_DIR, "myFile.opal")
Includes every .py
and .opal
file in a given directory. Expects a str
or path-like argument.
$includeDirectory os.path.join(HOME_DIR, "myFolder")
Defines a macro. A macro is a basic function that gets called with no overhead, since its body is copy-pasted into calls. Avoid using this too often since it can quickly increase the result file size. The body of the macro is anything between the $macro
statement and an $end
statement. Macros can be defined with no arguments...
$macro sayHi
IO.out("Hi!\n");
$end
... or with arguments. Arguments do not accept types and a default value cannot be set.
$macro add(a, b)
new int result = a + b;
$end
Macros are called using the $call
statement:
$call sayHi
$call add(2, 4)
Exports a block of code (all the way until an $end
statement) if a condition is True
.
package opal: import *;
$define GREET_USER True
$if GREET_USER
IO.out("Hi, user!\n");
$end
An $else
statement is also available:
package opal: import *;
$define GREET_USER True
$if GREET_USER
IO.out("Hi, user!\n");
$else
IO.out("Sorry, user. No greeting this time :(\n");
$end
The same action can also be performed through the comptime
statement, though it's much slower. $if
is useful for both simplicity and faster compile times, especially when working with large blocks of code.
Defines a block of code (all the way until an $end
statement) that will run during compilation. If an exception is thrown during this stage, the compiler will throw an error. In combination with the $export
and $exportBlock
statements, it can be used for conditional code generation. Example:
$define USE_CUPY False
$comptime
if USE_CUPY {
$export import cupy as numpy;
} else {
$exportBlock
import numpy;
?"running with numpy";
$end
}
$end
The COMPTIME_EXPORT_VARS
global dictionary can be used to create variables that can be read by export blocks. For example:
$define ADDING_WATER_DROPLETS True
$comptime
if ADDING_WATER_DROPLETS {
$export new int ONE_PLUS_ONE = 1;
} else {
COMPTIME_EXPORT_VARS["onePlusOneResult"] = str(1 + 1);
$export new int ONE_PLUS_ONE = onePlusOneResult;
}
$end
Tells the precompiler to directly transcribe code to the result until a $restore
statement. In practice, it allows to use Python or Cython code inside opal. Code in $nocompile
-$restore
blocks should be put on a "null indentation", for example:
if a != b {
if a < b {
$nocompile
for i in range(a, b):
if i > 2:
print(i)
$restore
}
}
This is needed because opal will add to the base indentation an inferred indentation, that is based on the code logic. This allows to directly import Python or Cython source files with no syntax errors.
Directly transcribes a line of code to the compiled result. Useful to avoid the $nocompile
-$restore
syntax for one-liners.
if a != b {
if a < b {
for i in range(a, b) {
if i > 2 {
$embed print(i)
}
}
}
}
Passes the compiler some default arguments. Supported arguments are:
--static, --nostatic, --nocompile, --compile-only, --type-mode, --require
Example:
$args ["--static", "--type-mode", "check", "--require", "2023.11.9"]
Creates Cython decorators if the compiler is transcompiling to Cython. Avoids errors when running a program in "Python mode". It uses the following syntax:
$cy flag_name value
and translates to:
@cython.flag_name(value)
... For example:
$cy nonecheck False
$cy cdivision True
inline:
new function divide(a: float, b: float) float {
return a / b;
}
Applies the Cython cdef
keyword to the next element when possible or forces a C definition on unknown types. For example:
$cdef
new function add(a: int, b: int) int {
return a + b;
}
$embed from cpython.mem cimport PyMem_Malloc, PyMem_Free
new function mallocTest() {
use PyMem_Malloc, PyMem_Free;
$cdef
new (int*) memory = <int*>PyMem_Malloc(20 * sizeof(int));
memory[0] = 2;
PyMem_Free(memory);
}
Since opal directly passes expressions to Python, that is, it doesn't parse them, Python operators are all usable, with a few additions:
!
: Equivalent to Python'snot
. If used at the beginning of a line with a variable name, it will invert the state of that variable:
!variable; # is equivalent to variable = !variable;
not variable; # this also produces the same result
||
: Equivalent to Python'sor
;&&
: Equivalent to Python'sand
;?
: It's used for debugging purposes. It prints the given expression and returns it:
myFunction(a, ?(b), c);
# the content of b will be printed
?c;
# the content of c will be printed
<-
: It's used to convert variables to a type during an assignment:
new int a = 2;
# type of a is int
float <- a = 3;
# type of a is float
dynamic <- a = Vector(2, 3);
# type of a is dynamic
Typing used with the arrow operator follows the same rules as types in the new statement.
++
and--
: They work as increments (respectively+= 1
and-= 1
). They are only allowed as statements, that is, they can't be used inside expressions. They can be used inside the first and last parts of a C-like for statement, and inside an inline type conversion (arrow operator). These syntaxes are all valid:
new int var = 0;
var++;
var--:
--var; # this "operator-first" syntax is only allowed as a statement alone,
# that is, for example, it won't work in a for loop.
++var;
float <- var++;