A Tour of Pyro
Pyro begs, borrows, and steals from every language I've ever used, but it drags Python down a back alley and leaves it naked, bleeding, and penniless. If you've used Python you'll find many aspects of Pyro's design philosophy familiar.
- Comments
- Variables
- Constants
- Numbers
- Strings
- Runes
- Booleans
- Nulls
- Equality
- Comparisons
- Printing
- Loops
- Conditions
- Functions
- Classes
- Vectors
- Maps
- Errors
- Panics
- Modules
- The Main Function
- Unpacking
- Source Code
- Testing
Comments
Comments begin with a #
symbol and run to the end of the line:
# Full line comment. var foo = 123; # Partial line comment.
Variables
Variables are declared using the var
keyword:
var foo = 123;
Variables must be declared before use. If an initial value isn't specified, the variable gets the default value null
, e.g.
var foo; assert foo == null;
You can declare multiple variables in a single statement, e.g.
var foo, bar; var baz = 123, bam = 456;
Variables have lexical scope and declarations inside blocks will shadow outer declarations, e.g.
var foo = 123; { var foo = 456; assert foo == 456; } assert foo == 123;
Identifier names (i.e. variables, constants, functions, and classes) should begin with an ASCII letter or an underscore and contain only ASCII letters, numbers, and underscores.
Names beginning with a $
symbol are special — this namespace is reserved by the language for builtin variables and functions.
Constants
Use the let
keyword to declare a constant, e.g.
let value = 123;
Constants work just like variables, but can only be assigned a value in their declaration statement, e.g.
let value = 123; value = 456; # syntax error
You can learn more about constants here.
Numbers
Pyro has two arithmetic number types: i64
for 64-bit signed two's-complement integers, and f64
for 64-bit IEEE 754 floats.
Both behave as you'd expect and support the usual range of arithmetic operations.
+
|
Addition. Returns an integer if both operands are integers or a float if either or both are floats. |
-
|
Subtraction (binary) or negation (unary). Subtraction returns an integer if both operands are integers or a float if either or both are floats. |
*
|
Multiplication. Returns an integer if both operands are integers or a float if either or both are floats. |
/
|
Floating-point division. Both operands will be converted to floats and the result will be a float. |
//
|
Truncating division. Returns an integer if both operands are integers or a float if either or both are floats. |
**
|
Power operator. Both operands are converted to floats and the result is a float. |
mod
|
Modulo operator. Both operands must be integers. |
rem
|
Remainder operator. Returns an integer if both operands are integers or a float if either or both are floats. |
Integer literals can use binary, octal, decimal, or hex notation:
var a = 0b101; # a == 5 var b = 0o101; # b == 65 var c = 101; # c == 101 var d = 0x101; # d == 257
Integer literals can use underscores to improve readability:
var foo = 123_456; var bar = 0b1010_0101;
Float literals require a decimal point and can also use underscores to improve readability:
var foo = 1.0; var pi = 3.141_592_654;
Strings
A string, str
, is an immutable array of bytes.
var string = "hello pyro";
Pyro strings have methods that let you manipulate them as ASCII or as UTF-8 but the string type itself is agnostic about its encoding — a string can contain any sequence of byte values including null bytes or invalid UTF-8.
You can find a full description of the string type here.
Runes
A rune, rune
, is an unsigned 32-bit integer representing a Unicode code point.
Rune literals use single quotes:
var r1 = 'a'; var r2 = '€'; var r3 = '🔥';
You can find a full description of the rune type here.
Booleans
A boolean value is either true
or false
.
You can convert any value to a boolean using the $bool()
function.
assert $bool(123) == true; assert $bool(null) == false;
Values which convert to true
are truthy, values which convert to false
are falsey.
-
The values
false
andnull
are falsey, as is anyerr
value. - All other values are truthy.
Nulls
Uninitialized variables have the default value null
.
var value; assert value == null;
You can use the null-coalescing operator ??
to supply a default value in place of a null
:
var result = maybe_null() ?? "default";
Functions which don't explicitly specify a return value have the default return value null
.
Equality
The equality operator is ==
. Its inverse is !=
.
Numerical types (i.e. i64
, f64
, and rune
) compare as equal using ==
if their values are numerically equal, e.g.
assert 1 == 1.0; assert 'a' == 97; assert 'b' == 98.0;
Strings compare as equal if they have the same content, e.g.
var foo = "foobar"; var bar = "foobar"; assert foo == bar;
Tuples compare as equal if they have the same length and their elements are equal, e.g.
var foo = ("foo", 123); var bar = ("foo", 123); assert foo == bar;
Sets compare as equal if they are set-equivalent, i.e. if they contain the same items in any order, e.g.
var foo = {1, 2, 3}; var bar = {3, 2, 1}; assert foo == bar;
By default, all other objects compare as equal only if they are the same object, e.g.
class Object {} var obj1 = Object(); var obj2 = Object(); assert obj1 == obj1; assert obj1 != obj2;
You can overload the equality operator to customize its behaviour for your own types.
Comparisons
You can use the comparison operators (<
, <=
, >
, >=
) with any combination of numerical types, e.g.
assert 2.0 > 1; assert 'a' > 42;
The comparison operators also work with strings, e.g.
assert "abc" < "def";
Strings are compared lexicographically by byte value, e.g.
assert "a" < "aa"; assert "aa" < "aaa";
You can use the comparison operators directly on tuples, e.g.
assert (1, 2, 3) < (1, 2, 4);
Tuples are compared lexicographically by element.
You can overload the comparison operators to customize their behaviour for your own types
Printing
The echo
statement prints to the standard output stream. It's useful for simple printing and debugging, e.g.
echo "hello pyro";
You can echo
any value — echo
stringifies its argument before printing it.
(It's equivalent to calling $str()
on the argument first and printing the result.)
echo 123;
You can supply multiple arguments to echo
, separated by commas, e.g.
echo foo, 123, "bar";
echo
prints the stringified values separated by spaces.
Pyro also has a family of $print()
/$println()
functions with support for format strings, e.g.
$print("hello pyro"); # "hello pyro" $print("{} and {}", "foo", "bar"); # "foo and bar" $print("{} and {}", 123, 456); # "123 and 456"
-
Calling the
$print()
function with a single argument is equivalent to calling$str()
on that argument first and printing the resulting string. -
Calling the
$print()
function with more than one argument is equivalent to calling$fmt()
on those arguments first and printing the resulting string.
The only difference between $print()
and $println()
is that $println()
adds a newline character to the output.
Pyro also has $eprint()
and $eprintln()
functions which print to the standard error stream.
See the string formatting documentation for a detailed look at the $fmt()
function and format strings.
Loops
Pyro has support for three different looping constructs. The simplest is the infinite loop:
var i = 0; loop { i += 1; if i == 5 { break; } } assert i == 5;
The loop
statement also supports C-style loops with an initializer, a condition, and an incrementing expression:
loop var i = 0; i < 10; i += 1 { echo i; }
The for
keyword in Pyro is used for looping over iterators:
var vec = ["foo", "bar", "baz"]; for item in vec { echo item; }
Finally, Pyro supports while
loops:
var i = 0; while i < 10 { i += 1; } assert i == 10;
All the looping constructs support break
and continue
.
Conditions
Conditional statements in Pyro look like this:
if money > 100 { echo "we have lots of money"; }
An if
statement evaluates the truthiness of an expression. The syntax is:
if <expression> { ... }
As you'd expect, if
statements support optional else if
and else
clauses:
if money > 100 { echo "we have lots of money"; } else if money > 10 { echo "we have some money"; } else { echo "we're poor"; }
Pyro also supports conditional-scoped variables and conditional expressions.
Functions
Function definitions look like this:
def add(a, b) { return a + b; }
If you don't explicitly return a value from a function, its return value is null
.
Pyro has full support for closures. An inner function declared inside an outer function can capture the outer function's local variables, including its parameters, e.g.
def make_adder(increment) { def adder(num) { return num + increment; } return adder; } var adds_one = make_adder(1); assert adds_one(0) == 1; assert adds_one(1) == 2; var adds_two = make_adder(2); assert adds_two(0) == 2; assert adds_two(1) == 3;
Functions are first-class citizens in Pyro, meaning you can pass them around just like any other value. You can also declare and use functions anonymously, e.g.
var add = def(a, b) { return a + b; }; assert add(1, 2) == 3; assert add(3, 4) == 7;
You can find full documentation for Pyro's functions here.
Classes
Class definitions look like this:
class Person { pub var name; pub var role = "programmer"; def $init(name) { self.name = name; } pub def info() { return "${self.name} is a ${self.role}."; } }
Create an instance of a class by calling its name, e.g.
var person = Person("Dave"); assert person.name == "Dave";
Arguments are passed to the (optional) initializer method, $init()
.
Call a method on an instance using the method access operator, :
, e.g.
assert person:info() == "Dave is a programmer."
Get or set an instance's fields using the field access operator, .
, e.g.
person.role = "pointy headed manager"; assert person:info() == "Dave is a pointy headed manager.";
Boo Dave.
Classes are a big enough topic to deserve a page of their own which you can find here.
Vectors
Pyro has a builtin vector type, vec
— a dynamic array similar to a list
in Python. You can create a vec
using a vector-literal, e.g.
var vec = ["foo", "bar"];
Vectors use zero-based indices, e.g.
assert vec[0] == "foo"; assert vec[1] == "bar";
You can index into a vector to get or set entries, e.g.
vec[0] = "baz"; assert vec[0] == "baz";
You can add items to a vector using the :append()
method, e.g.
vec:append("bam"); assert vec[2] == "bam";
You can find a full description of Pyro's vec
type here.
Maps
Pyro has a builtin map type, map
— an ordered hash-map similar to a dict
in Python. You can create a map
using a map-literal, e.g.
var map = { "foo" = 123, "bar" = 456, };
You can index into a map
to get or set entries, e.g.
assert map["foo"] == 123; assert map["bar"] == 456; map["baz"] = 789; assert map["baz"] == 789;
You can find a full description of Pyro's map type here.
Errors
Pyro has a dedicated error type, err
, which a function can return to indicate failure, e.g.
def str_len(arg: str) -> i64|err { if $is_str(arg) { return arg:byte_count(); } return $err("argument is not a string"); }
You can check if a value is an err
using the $is_err()
function, e.g.
var result = str_len("foo"); if $is_err(result) { $exit("error: ${result}"); } echo result;
Alternatively, you can provide a default value for a function call that might fail using the error-coalescing operator !!
, e.g.
var result = str_len("foo") !! 0;
You can find a full description of the err
type here.
Panics
A panic in Pyro is similar to an exception in other languages — it indicates that the program has attempted to do something impossible like divide by zero or read from a file that doesn't exist.
An untrapped panic will cause the program to exit with an error message, a stack trace, and a non-zero status code.
You can trap a panic using a try
expression, e.g.
var result = try might_panic();
A try
expression returns the value of its operand if it evaluates without panicking or an err
if it panics.
try
is a unary operator with the same precedence as !
or -
so you should wrap lower-precedence expressions in brackets, e.g.
var foo = try (1 / 2); # 0.5 var bar = try (1 / 0); # err
You can use the error-coalescing operator !!
to provide a default value for a panicking expression, e.g.
var foo = try (1 / 2) !! 0; # 0.5 var bar = try (1 / 0) !! 0; # 0
You can raise a panic from your own code by calling the $panic()
function with an error message, e.g.
$panic("oh no!");
If the panic is not trapped, the error message will be printed to the standard error stream and the program will exit with a non-zero status code.
Modules
A module is a Pyro file loaded as a library. Modules are loaded using the import
keyword.
Assume we have a file called math.pyro
containing math functions:
import math; var foo = math::abs(-1);
Modules can contain submodules. Assume we have a directory called math
containing a file called trig.pyro
:
import math::trig; var foo = trig::cos(1);
Modules are a big enough topic to deserve a page of their own which you can find here.
The Main Function
When you run a script file, Pyro first executes the script then looks for a function called $main()
.
If it finds a $main()
function it runs it automatically.
def $main() { echo "hello from $main()"; }
Note that the $main()
function won't be run if you import the same file as a module.
Unpacking
You can unpack the values of a tuple or vector in a var
declaration using ()
, e.g.
var (foo, bar, baz) = $tup(123, 456, 789); assert foo == 123; assert bar == 456; assert baz == 789;
You can discard unwanted values by assigning them to the unnamed variable _
, e.g.
var (foo, _, baz) = $tup(123, 456, 789); assert foo == 123; assert baz == 789;
You can partially unpack the leading values, ignoring the remainder, e.g.
var (foo, bar) = $tup(123, 456, 789); assert foo == 123; assert bar == 456;
Similarly, you can unpack the loop variable of a for
loop if it's a tuple or vector, e.g.
var map = { "foo" = 123, "bar" = 456, }; for (key, value) in map { echo key; echo value; }
This works because iterating over a map
returns (key, value)
tuples.
Source Code
Source code outside of string literals is assumed to be UTF-8 encoded. String literals can contain arbitrary byte sequences including null bytes and invalid UTF-8.
Testing
Pyro has builtin support for unit tests.
Use the assert
statement to test your code, e.g.
def add(a, b) { return a + b; } assert add(1, 2) == 3; assert add(3, 4) == 7;
An assert
statement will panic if its operand expression evaluates as falsey. The syntax is:
assert <expression>;
You can use the test
command to run test functions, e.g.
def $test_addition() { assert add(1, 2) == 3; assert add(3, 4) == 7; }
You can find documentation for the test
command here.