Varyx (A Programming Language)
Introduction
Varyx is a general-purpose, high-level programming language in development since mid-2015.
Summary:
- C-like syntax and structure
- automatic memory management, including timely destruction of resources
- big integers
- closures
- optional type annotations
- interpreted execution
Future Goals:
- translation to other source languages (e.g. C++, JS)
- safely run hostile code
- nested sandboxes — allow untrusted code to create its own sandboxes to run other code that it doesn’t trust
Components
vc
, the Varyx calculator, which has all the features of the Varyx programming language (e.g. code blocks) but no access to the host environment except for its arguments and the result.vx
, the Varyx executor, a conventional scripting language interpreter that can execute either a script file or an inline script.
The Varyx source code is AGPL-licensed and published on GitHub.
Design Principles
- Code should be pleasing to write and to read — not just understandable, but visually appealing.
- Reuse good ideas; reject bad ones. Occasionally invent new ones.
- The language lives in service to its users:
- Language processors should do their best to protect their users from error. Languages should be designed to encourage this. It’s better to catch errors at compile time than run time. It’s better to halt than continue a flawed program.
- Languages should encourage their users to do the right thing. Don’t make the right thing expensive or annoying (or outright impossible).
- Ultimately, the user is in control, and a language should get out of the way when asked politely. That said, users who disrespect the language do so at their own risk.
Synopsis
All the following samples are valid Varyx code and currently supported.
Every value in Varyx is a list of zero or more items. Every item 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
The type of a list is simply a list of the types of each of its items.
Lists are not items, and therefore lists can’t contain lists. A list is just a group of items. 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 items.
# empty list
()
Since parentheses are otherwise optional, there’s no distinction between an item and a list of that item.
# item / list of one item
"This is an item"
Containers
An array is a sequence of items.
# array
[ 1, 2, 5 ]
[ "egg", "sausage", "bacon" ]
Arrays are heterogenous, i.e. they can contain items of different types.
[ 1, 2, "red", "blue" ]
[ null, false, 0, '\0', "" ]
Arrays are themselves items, and can contain other arrays.
[ ["egg", "bacon"], ["egg", "sausage", "bacon"], ["egg", "spam"] ]
A mapping is a kind of dyad — an item that contains two other items,
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 (var
) or constant (let
/ const
).
# symbols
var total = 0
let combo = 12345
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
combo = 12345
Containers store elements by value, so you can’t assign to an element of a container which is stored in a constant symbol.
let 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 items 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 }
let 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
}
let 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)
{
let 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
}
}