Pyro

An experimental programming language.

Version 0.3.2

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

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 has the value null.

var foo;
assert foo == null;

You can declare multiple variables in a single statement:

var foo, bar;
var baz = 123, bam = 456;

Variables have lexical scope and declarations inside blocks will shadow outer declarations:

var foo = 123;

{
    var foo = 456;
    assert foo == 456;
}

assert foo == 123;

Identifier names (this includes variables, functions, and classes) should begin with a letter or an underscore and contain only letters, numbers, and underscores.

Names beginning with a $ symbol are $pecial — this namespace is reserved by the language for builtin variables and functions.

Numbers

Pyro has two numeric 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 numeric 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.
% Modulo/remainder operator. 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.

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 a = 123_456;
var b = 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.

Characters

A character, $char, is an unsigned 32-bit integer representing a Unicode code point.

Character literals use single quotes:

var c1 = 'a';
var c2 = '€';
var c3 = '🔥';

You can find a full description of the character type here.

Booleans

A boolean value is either true or false. You can convert any value to a boolean using the $bool(arg) 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 and null are falsey, as is any $err value, the integer value 0, and the floating-point value 0.0. All other values are truthy.

Nulls

Uninitialized variables have the default value null.

var foo;
assert foo == null;

You can use the null-coalescing operator ?? to supply a default value in place of a null:

var foo = maybe_null() ?? "default";

Equality

Pyro has a strict concept of equality — two objects are equal if they have the same type and the same value.

assert 1 == 1;        # True.
assert 1.0 == 1.0;    # True.
assert 1 == 1.0;      # False. $i64 != $f64.

Note that characters don't compare equal to their integer value without conversion:

assert 'a' == 'a';          # True.
assert 'a' == 61;           # False. $char != $i64.
assert $i64('a') == 61;     # True.
assert 'a' == $char(61);    # True.

Note that a $map can have three distinct keys with the values 61, 61.0, and 'a', even though the values are numerically equivalent. (Pyro's strict concept of equality is partly designed to enable fast map lookups without needing to consider all possible conversions.)

Strings are equal if they have the same content:

var foo = "foobar";
var bar = "foobar";
assert foo == bar;

Tuples are equal if they have the same length and their elements are equal:

var foo = $tup("foo", 123);
var bar = $tup("foo", 123);
assert foo == bar;

Other objects are equal only if they are the same object:

class Foo {}

var foo1 = Foo();
var foo2 = Foo();

assert foo1 == foo1;    # True.
assert foo1 == foo2;    # False.

Printing

The echo statement prints to the standard output stream. It's useful for simple printing and debugging.

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.)

Pyro also has a family of $print() functions with support for format strings:

$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 it with more than one argument is equivalent to calling $fmt() on those arguments first and printing the resulting string.

See the string formatting documentation for a detailed look at the $fmt() function and format strings.

Loops

Pyro has support for several 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 reserved 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";
}

Conditional statements evaluate the truthiness of their conditions. As you'd expect, they 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 has support for conditional expressions using the ternary operator ?|:

var status = money > 100 ? "rich" | "poor";

Functions

Function definitions look like this:

def add(a, b) {
    return a + b;
}

Inner functions declared inside outer functions can capture the outer function's local variables, including its parameters:

def make_adder(increment) {
    def adder(num) {
        return num + increment;
    }
    return adder;
}

var adds_one = make_adder(1);
adds_one(0);  # 1
adds_one(1);  # 2

var adds_two = make_adder(2);
adds_two(0);  # 2;
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:

var add = def(a, b) {
    return a + b;
};

add(1, 2);  # 3
add(3, 4);  # 7

Classes

Class definitions look like this:

class Person {
    var name;
    var occupation = "programmer";

    def $init(name) {
        self.name = name;
    }

    def info() {
        echo self.name + " is a " + self.occupation + ".";
    }
}

Create an instance of a class by calling its name:

>>> var dave = Person("Dave");

Arguments are passed on to the optional initializer method, $init().

Call a method on an instance using the method access operator, ::

>>> dave:info();
Dave is a programmer.

Get or set an instance's fields using the field access operator, .:

>>> dave.occupation = "pointy headed manager";
>>> dave:info();
Dave is a pointy headed manager.

Boo Dave.

Classes are complex enough to deserve a page of their own which you can find here.

Errors

Pyro has an error type, $err, which a function can return to indicate failure. An $err is a specialized kind of tuple under the hood so it supports all the same methods and operators as a $tup.

var foo = $err();
var bar = $err("oops!", 123);

You can check if a value is an $err using the $is_err(arg) function. Alternatively, you can provide a default value for a function call that might fail using the error-coalescing operator !!:

var foo = might_fail() !! "default";

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 unhandled panic will result in the program exiting with an error message and a non-zero status code.

You can catch a panic using a try expression:

var foo = 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:

var foo = try (1 / 1);  # 1
var bar = try (1 / 0);  # $err

Note that you can use the error-coalescing operator !! to provide a default value for a panicking expression:

var foo = try (1 / 1) !! 0;  # 1
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:

$panic("stop that!");

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 complex enough 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.

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, error, or vector in a var declaration using the following syntax:

var (foo, bar, baz) = $tup(123, 456, 789);

assert foo == 123;
assert bar == 456;
assert baz == 789;

You can similarly unpack the loop variable of a for loop:

var map = {
    "foo" = 123,
    "bar" = 456,
};

for (key, value) in map:entries() {
    echo key;
    echo value;
}

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.