Varyx (A Programming Language)


Introduction

Varyx is a general-purpose, high-level programming language in development since mid-2015.

Summary:

Future Goals:

Components

The Varyx source code is AGPL-licensed and published on GitHub.

Design Principles

Synopsis

All the following samples are valid Varyx code and currently supported.

Every value in Varyx is a list of zero or more things. Every thing has a type.

Basic types

# integer
12345, -6, 27742317777372353535851937790883648493, 0xFFFD, 0b101010

# string
"", "Hello world\n", "NULs\000embedded\x00here\0"

# packed
x"", x"4E75", x"14def9dea2f79cd65812631a5cf5d3ed", b"101010"

# byte
'A', '\n', '\0', '\007', '\xFF'

# boolean
true, false

# null
null

Lists are not things, and therefore lists can’t contain lists. A list is just a group of things. What defines a group is proximity. If you place two groups together, you get one larger group.

# these are all the same value
1, 2, 3, 4, 5
(1, 2, 3, 4, 5)
(1, 2), 3, (4, 5)
(((1, 2), (3)), (4, (), 5))

Parentheses are optional and may be used for grouping in the same manner as other expressions — with one exception: The empty list can only be written with parentheses. It’s a list of zero things.

# empty list
()

Since parentheses are otherwise optional, there’s no distinction between a thing and a list of that thing.

# thing / list of one thing
"It’s a thing"

Containers

An array is a sequence of things.

# array
[ 1, 2, 5 ]
[ "egg", "sausage", "bacon" ]

Arrays are heterogenous, i.e. they can contain things of different types.

[ 1, 2, "red", "blue" ]
[ null, false, 0, '\0', "" ]

Arrays are themselves things, and can contain other arrays.

[ ["egg", "bacon"], ["egg", "sausage", "bacon"], ["egg", "spam"] ]

A mapping is a kind of dyad — a thing that contains two other things, a key and a value. The key can’t be null.

# mapping
"one" => 1
"two" => 2
7 => "seven"
8 => "eight"
"false" => false
true => "true"

Another mapping syntax will automatically quote bareword keys as strings. It does nothing to values, or to keys that aren’t barewords.

one: 1
two: 2
7: "seven"
8: "eight"
false: "This key is the string 'false'"
(true): "This key is the value `true`"

A mapping can contain an array:

squares: [ 0, 1, 4, 9, 16, 25 ]
primes:  [ 2, 3, 5, 7, 11, 13 ]

And an array can contain mappings:

[ 1: "who", 2: "what", 3: null ]

A table is an associative array. It’s like an array of mappings, but it emphasizes finding keys and retrieving the corresponding values. To create one, you have to provide an array of mappings and additionally specify the key type.

# table
string^[ Varyx: "table", Perl: "hash", Python: "dict" ]
integer^[ 0: "stdin", 1: "stdout", 2: "stderr", 3: "/etc/motd" ]

[TODO: Replace and deprecate this syntax.]

Symbols

A symbol is a named location for a value. A symbol must be declared before it’s used. Once declared, it can be assigned a value, until which it’s undefined. Symbols are either variable or constant.

# symbols
var total = 0
const url = "https://www.vcode.org/"

A constant symbol can’t be assigned a value more than once (even if the second assignment repeats the value).

# these are okay
++total
total += 10
total = 100

# this is not okay
url = "https://www.vcode.org/"

Containers store elements by value, so you can’t assign to an element of a container which is stored in a constant symbol.

const counting = [1, 2, 3]

# this is right out
counting[ 2 ] = 5

However, the undefined state is not a value, so it’s possible to declare a constant in one place and define it in another.

var i = 0
const foo

# i is modified here
...

foo = i

Any access of a symbol in an undefined state (other than assigning it a value) is an error.

Symbols are not things or even values, and therefore can’t be stored within containers or in other symbols. When a symbol is used in a context that requires a value, the value stored in the symbol is substituted.

Functions

Varyx has functions as first-class objects (which means you can store them in arrays, for example). Most often you’ll define one and give it a name with def:

def cube (x)
{
    return x^3
}

Following the function name is its prototype, which lists the names of the function’s parameters. The parameter values are filled in by the arguments passed by the function’s caller.

def is_pythagorean_triple (a, b, c)
{
    return a^2 + b^2 == c^2
}

# prints "true"
print is_pythagorean_triple( 3, 4, 5 )

You can return a function from another function:

def get_handler (c)
{
    def add (a, b) { return a + b }
    def sub (a, b) { return a - b }
    def mul (a, b) { return a * b }
    def div (a, b) { return a / b }

    const ops = byte^
    [
        '+': add,
        '-': sub,
        '*': mul,
        '/': div,
    ]

    return ops[ c ]
}

# prints "7"
print get_handler( '+' )( 3, 4 )

Functions are closures, which means they can access variables defined in an outer function, even after it’s exited:

def make_counter
{
    var i = 0

    def counter
    {
        return ++i
    }

    return counter
}

const count = make_counter()

# Prints 1, 2, and 3 on separate lines
print count()
print count()
print count()

# Prints 1 again
print make_counter()()

# Prints 4
print count()

Function prototypes can be omitted when you don’t need to look at the arguments. An anonymous block is a simpler form of closure. We could have used one above:

def make_counter
{
    var i = 0

    return { ++i }
}

You can alter the flow of control with branching and looping structures. Here’s branching with if/then:

def th (n)
{
    const ones = abs n % 10

    if ones > 3 then
    {
        return "th"
    }

    return ["th", "st", "nd", "rd"][ ones ]
}

def ordinal (n)
{
    return string( n, th(n) )
}

# returns "42nd"
ordinal( 42 )

And here’s looping with for. It declares a symbol which you can’t modify, but its value changes to each member of the sequence in turn.

for x in ["bacon", "egg", "spam", "sausage"] do
{
    if x == "spam" then
    {
        print "That’s got spam in it!"
        break
    }
}