Introduction to Aurora

Aurora is a powerful yet remarkably simple programming language designed for scripting and automation. With its Lua/Ruby-like syntax and dynamic typing, Aurora offers an easy learning curve for users while enabling them to tackle complex tasks efficiently. It aims to replace the long-standing (mis)use of shell scripts for intricate operations.

Features

  • Lua/Ruby-like syntax: Aurora adopts a syntax reminiscent of popular scripting languages Lua and Ruby, making it familiar and approachable to developers.
  • Dynamic typing: The language supports dynamic typing, allowing variables to hold values of different types throughout their lifetime.
  • Extraordinarily simple: Aurora prioritizes simplicity, emphasizing straightforwardness and ease of use.

Example

# This is a comment

# This is a subroutine
sub hello name
    print fmt"Hello, {name}!"
end

# This is a function
fn add a, b
    return a + b
end

# This is a variable
name = "world"

# This is a top-level call (subroutine or function)
hello name # Hello, world!

print add(1, 2) # Cannot call a subroutine like this; only functions

Functions vs. Subroutines

In Aurora, there are two types of top-level calls: functions and subroutines.

  • Functions: Functions are called using the syntax function_name(arguments, ...). They can be invoked from anywhere in the program, including within expressions. Functions can return values and are defined using the fn keyword.
  • Subroutines: Subroutines are called using the syntax subroutine_name arguments, .... They can only be called from the top level of the program and cannot be invoked within expressions. Subroutines do not return values and are defined using the sub keyword.

Variables

Aurora employs scoping rules similar to those found in most C-like languages.

# This is a global variable
global = "global"

# This is a 'do' block; an anonymous subroutine
do 
    # This is a local variable
    local = "local"
end 

# The 'do' block is called here, as it's at the top level

print global # Output: global
print local # Error: undefined variable 'local'

Nil

In Aurora, the absence of a nil value means that functions must always return a value. Consequently, when a function does not have a specific value to return, it is declared as a subroutine. By designating such functions as subroutines, Aurora ensures clarity and avoids confusion. Subroutines are meant to execute actions or operations without returning a value, highlighting their purpose of performing tasks rather than producing specific outputs. This distinction allows developers to write more precise and understandable code, enhancing code organization and reducing potential errors related to missing or unexpected return values.

Types

Aurora includes the following types:

  • str: Represents a string of characters.
  • num: Represents a numeric value.
  • bool: Represents a boolean value (true or false).
  • list: Represents a collection of values.
  • fn: Represents a function.
  • sub: Represents a subroutine.
  • map: Represents a mapping of keys to values.

Aurora's concise syntax and simplicity make it an appealing choice for scripting and automating tasks efficiently.

Aurora vs. Shell

Aurora outshines traditional shell scripting with its focus on readability and simplicity. Its intuitive syntax, plain English function names, and clean structure enhance code comprehension. Aurora's built-in functions and higher-level abstractions simplify complex tasks, reducing convoluted command chains. This results in concise, straightforward code that is easier to understand and maintain. By prioritizing readability and simplicity, Aurora offers an approachable and error-resistant scripting experience, boosting productivity.

#!/bin/bash

directory="/path/to/directory"

echo "Listing files in $directory:"

for file in "$directory"/*; do
    if [ -f "$file" ]; then
        filename=$(basename "$file")
        size=$(du -sh "$file" | awk '{print $1}')
        echo "File: $filename | Size: $size"
    fi
done
#!/bin/aurora

directory = "/path/to/directory"

print fmt"Listing files in {directory}:"

for file, dir(directory)
    if not isDir(file)
        print fmt"File: {basename(file)} | Size: {size(file, 'kb)}"
    end
end

Control Flow

Aurora has the control flow you'd expect from a modern language. It also has a few features that are (somewhat) unique to Aurora.

If Statements

If statements are pretty standard.

if condition
    # do something
end

If statements can also have an else clause.

if condition
    # do something
else
    # do something else
end

Unless Statements

Unless statements are the opposite of if statements.

unless condition
    # do something
end

Unless statements can also have an else clause.

unless condition
    # do something
else
    # do something else
end

While Loops

While loops are pretty standard.

while condition
    # do something
end

Until Loops

Until loops are the opposite of while loops.

until condition
    # do something
end

For Loops

For loops iterate over a list. For doing something a certain number of times, see times.

for i, upTo(10)
    # do something
end

Break and Continue

Break and continue are pretty standard.

while condition
    if condition
        break
    end
    if condition
        continue
    end
end

Switch Statements

Switch statements are pretty standard.

switch value
    case 1
        # do something
    case 2
        # do something else
    else
        # do something else
end

Select Statements

Select statements are like a chain of if statements.

select
    case condition1
        # do something
    case condition2
        # do something else
    else
        # do something else
end

Functions and Subroutines

In Aurora, functions and subroutines are defined using the fn and sub keywords, respectively.

Functions

Functions are defined using the fn keyword, followed by the function's name, its parameters, and its body. It must always return a value.

fn add a, b
    return a + b
end

# or, using the shorthand syntax:
fn add a, b -> a + b

Subroutines

Subroutines are defined using the sub keyword, followed by the subroutine's name, its parameters, and its body. Subroutines cannot return a value. They can return early using the return keyword, but it must not be followed by any value.

sub say_hi name
    print "Hello, ", name
end

# or, using the shorthand syntax:
sub say_hi name -> print "Hello, ", name

Anonymous Functions and Subroutines

Functions and subroutines can also be anonymous. They are do and lambda blocks, respectively.

# Anonymous function:
add = lambda a, b -> a + b
# or:
add = lambda a, b
    return a + b
end

# Anonymous subroutine:
say_hi = do name
    print "Hello, ", name
end

Calling Functions and Subroutines

There are two places where functions can be called: in expressions and in statements. At the top level, functions can be called, just discarding the result. However, subroutines can only be called in statements, as they produce no result.

# Calling a function in an expression:
print add(1, 2) # 3

# Calling a function at the top level:
add 1, 2 # Discards the result, but still executes the function

# Calling a subroutine in a statement:
print "Hello, World!"

Top-Level Calls

Top-level calls don't have to be to a named function or subroutine. They can also be calls to do blocks or lambda block expressions, or any other expression (list indexing, for example).

If an expression is at the top level, it is immediately evaluated, and the result must be a callable value.

# Calling a do block:
do
    print "Hello, World!"
end  # It is immediately executed, as it's at the top level

foo = [print]
foo:0 "Hello, World!" # Calling a subroutine in a list

Literal Values

Aurora has a few literal values that can be used in expressions.

String Literals

String literals are surrounded by double quotes ("), or backticks (`). There is also a shorthand syntax for string literals, which is the ' character followed by a single word. It does not have an end delimiter.

"Hello, world!" # A string literal
`Hello, world!` # Also a string literal
'hello # Shorthand syntax for string literals, known as a symbol in other languages

You can do string interpolation with the fmt operator. It takes a string, with expressions inside curly braces ({}).

name = "world"
print fmt"Hello, {name}!"

Number Literals

Number literals are pretty standard. They can be written in decimal, hexadecimal, octal, or binary. They can also be written in scientific notation.

123 # Decimal
0x123 # Hexadecimal
0o123 # Octal
0b101 # Binary
1.23e2 # Scientific notation

Boolean Literals

Boolean literals are either true or false.

true
false

List Literals

List literals are surrounded by square brackets ([]). They can contain any number of values, separated by commas.

[1, 2, 3]

Map Literals

Map literals are surrounded by curly braces ({}). They can contain any number of key-value pairs, separated by commas, or newlines.

{
    foo -> 2, bar -> 4
    baz -> 6
}

Variables

Variables in Aurora are defined using no keywords, but just the variable's name and its value.

a = 1

Variables can be reassigned.

a = 1
a = 2

Variables can be reassigned to a different type.

a = 1
a = "Hello, world!"

You can add, subtract, multiply, and divide variables.

a = 1
a += 1 # 2
a -= 1 # 1
a *= 2 # 2
a /= 2 # 1

Variables are scoped to the block they are defined in, or globally if defined at the top level.

a = 1
if true
    a = 2
end
print a # 2

do
    b = 1
end
print b # Error: b is not defined

Constants

Constants are denoted by an uppercase first letter.

A = 1

Constants cannot be reassigned.

A = 1
A = 2 # Error: A is a constant

Lists

Lists are declared as comma-seperated values between two square brackets.

[1, 2, 3]

Lists can be indexed using the : operator.

a = [1, 2, 3]
print a:0 # 1
print a:1 # 2
print a:2 # 3

You can assign to a list index.

a = [1, 2, 3]
a:0 = 4
print a # [4, 2, 3]

See List Functions for functions that operate on lists.

Maps

Maps are a type of collection that store key-value pairs. They are also known as dictionaries or associative arrays in other languages. In Aurora, maps are surrounded by curly braces ({}), and contain key-value pairs separated by commas or newlines.

{
    foo -> 2, bar -> 4
    baz -> 6
}

Maps can be accessed using either the : operator, or the :: operator.

m = {
    foo -> 2, bar -> 4
    baz -> 6
}
print m:"foo" # 2
print m::foo # 2

You can assign to a map index.

m = {
    foo -> 2, bar -> 4
    baz -> 6
}
m::foo = 3 # or m:"foo" = 3
print m # {foo -> 3, bar -> 4, baz -> 6}

See Map Functions for functions that operate on maps.

Operators

Precedence Table

PrecedenceOperatorDescription
1-, fmtUnary minus, string formatting
2(), :, ::Function call, index, map index
3notLogical not
4*, /, %, .Multiplication, division, mod, apply operator
5+, -Addition, subtraction
6==, !=, <, <=, >, >=Comparison
7andLogical and
8orLogical or

Arithmetic Operators

1 + 1 # 2
1 - 1 # 0
1 * 1 # 1
1 / 1 # 1
1 % 1 # 0

Comparison Operators

1 == 1 # true
1 != 1 # false
1 < 1 # false
1 <= 1 # true
1 > 1 # false
1 >= 1 # true

Logical Operators

true and false # false
true or false # true
not true # false

String Formatting Operator

The string formatting operator (fmt) is used to format a string. It takes a string, with expressions inside curly braces ({}).

name = "world"
print fmt"Hello, {name}!"

Apply Operator

The apply operator (.) is used to call a function on a value. It basically re-orders it's operands, so f.g(x) is equivalent to g(f, x). The parentheses are optional if the function takes only one argument.

fn plus_one num -> num + 1
print 1.plus_one # 2

fn add num1, num2 -> num1 + num2
print 1.add(2) # 3

Function Call Operator

The function call operator (()) is used to call a function. It has a comma-separated list of arguments inside the parentheses.

fn plus_one num -> num + 1
print plus_one(1) # 2

Index Operator

The index operator (:) is used to index a list or map.

a = [1, 2, 3]
print a:0 # 1
print a:1 # 2
print a:2 # 3

m = {
    foo -> 2, bar -> 4
    baz -> 6
}
print m:"foo" # 2

Map Index Operator

The map index operator (::) is used to index a map without quotes.

m = {
    foo -> 2, bar -> 4
    baz -> 6
}
print m::foo # 2

The Standard Library

The standard library is a collection of functions and variables that are distributed with the Aurora programming language. These items are imported by default in every Aurora program.

Conventions

Functions which do a "destructive" operation (i.e. they modify their arguments, manipulate the filesystem in a possibly destructive way, etc.) are suffixed with an exclamation mark (!). Functions which "ask" a question (i.e. they return a boolean value) are suffixed with a question mark (?).

Type Signatures

The type signatures of functions and variables are written in a pseudo-Aurora syntax. ... means that the function or variable takes a variable number of arguments.

Categories

The standard library is divided into these categories:

  • I/O: Input and output functions.
  • File: File manipulation functions.
  • String: String manipulation functions.
  • Math: Mathematical functions and constants.
  • Time: Time and date functions.
  • Type: Type checking and conversion functions.
  • List: List manipulation functions.
  • Misc: Miscellaneous functions.

Standard Library - I/O Functions

print

sub print any...

Description

Prints the given arguments to the standard output stream, with no separator between them.

Example

print "Hello, ", "world!"

ask

fn ask str -> str

Description

Asks the user for input and returns it.

Example

name = ask("What is your name? ")
print "Hello, ", name

read

fn read str -> str

Description

Reads the contents of a file and returns it as a string.

Example

contents = read("file.txt")
print contents

Standard Library - File Functions

copy!

sub copy! str str

Description

Copies a file from one path to another. Will overwrite the destination file if it already exists.

Example

copy! `file.txt`, `file2.txt`

copy

fn copy str str -> bool

Description

Copies a file from one path to another. Will not overwrite the destination file if it already exists. Returns true if the file was copied, false otherwise.

Example

copy `file.txt`, `file2.txt`

# error-checking
unless copy(`file.txt`, `file2.txt`)
    print "Failed to copy file!"
end

move!

sub move! str str

Description

Moves a file from one path to another. Will overwrite the destination file if it already exists.

Example

move! `file.txt`, `file2.txt`

move

fn move str str -> bool

Description

Moves a file from one path to another. Will not overwrite the destination file if it already exists. Returns true if the file was moved, false otherwise.

Example

move `file.txt`, `file2.txt`

# error-checking
unless move(`file.txt`, `file2.txt`)
    print "Failed to move file!"
end

delete!

sub delete! str

Description

Deletes a file.

Example

delete! `file.txt`

new!

sub new! str

Description

Creates a new file. Will overwrite the file if it already exists.

Example

new! `file.txt`

new

fn new str -> bool

Description

Creates a new file. Will not overwrite the file if it already exists. Returns true if the file was created, false otherwise.

Example

new `file.txt`

# error-checking
unless new(`file.txt`)
    print "Failed to create file!"
end

exists?

fn exists? str -> bool

Description

Returns true if the file exists, false otherwise.

Example

if exists?(`file.txt`)
    print "File exists!"
end

write!

sub write! str str

Description

Writes a string to a file. Will overwrite the file if it already exists.

Example

write! `file.txt`, "Hello, world!"

write

fn write str str -> bool

Description

Writes a string to a file. Will not overwrite the file if it already exists. Returns true if the file was written to, false otherwise.

Example

write `file.txt`, "Hello, world!"
   
# error-checking
unless write(`file.txt`, "Hello, world!")
    print "Failed to write to file!"
end

Standard Library - String Functions

len

fn len str -> num

Description

Returns the length of the given string.

Example

print len("abc") # 3

split

fn split str, str -> [str]

Description

Splits the given string into substrings at each occurrence of the given separator and returns them as an array.

Example

print split("a,b,c", ",") # ["a", "b", "c"]

replace

fn replace str, str, str -> str

Description

Replaces all occurrences of the given substring in the given string with the given replacement.

Example

print replace("abcabc", "a", "x") # "xbcxbc"

substring

fn substring str num num -> str

Description

Returns the substring of the given string starting at the given index and ending at the given index.

Example

print substring("abc", 1, 2) # "bc"

Standard Library - Math Functions

Note

All of these functions are declared under the global math object.

sin

fn sin num -> num

Description

Returns the sine of the given angle in radians.

Example

print math::sin(0) # 0
print math::sin(1) # 0.8414709848078965

cos

fn cos num -> num

Description

Returns the cosine of the given angle in radians.

Example

print math::cos(0) # 1
print math::cos(1) # 0.5403023058681398

tan

fn tan num -> num

Description

Returns the tangent of the given angle in radians.

Example

print math::tan(0) # 0
print math::tan(1) # 1.5574077246549023

asin

fn asin num -> num

Description

Returns the arc-sine of the given angle in radians.

Example

print math::asin(0) # 0
print math::asin(1) # 1.5707963267948966

acos

fn acos num -> num

Description

Returns the arc cosine of the given angle in radians.

Example

print math::acos(0) # 1.5707963267948966
print math::acos(1) # 0

atan

fn atan num -> num

Description

Returns the arc tangent of the given angle in radians.

Example

print math::atan(0) # 0
print math::atan(1) # 0.7853981633974483

sinh

fn sinh num -> num

Description

Returns the hyperbolic sine of the given angle in radians.

Example

print math::sinh(0) # 0
print math::sinh(1) # 1.1752011936438014

cosh

fn cosh num -> num

Description

Returns the hyperbolic cosine of the given angle in radians.

Example

print math::cosh(0) # 1
print math::cosh(1) # 1.5430806348152437

tanh

fn tanh num -> num

Description

Returns the hyperbolic tangent of the given angle in radians.

Example

print math::tanh(0) # 0
print math::tanh(1) # 0.7615941559557649

asinh

fn asinh num -> num

Description

Returns the inverse hyperbolic sine of the given angle in radians.

Example

print math::asinh(0) # 0
print math::asinh(1) # 0.881373587019543

acosh

fn acosh num -> num

Description

Returns the inverse hyperbolic cosine of the given angle in radians.

Example

print math::acosh(0) # NaN
print math::acosh(1) # 0

atanh

fn atanh num -> num

Description

Returns the inverse hyperbolic tangent of the given angle in radians.

Example

print math::atanh(0) # 0
print math::atanh(1) # NaN

exp

fn exp num -> num

Description

Returns the exponential of the given number. The exponential function is defined as e^x, where e is Euler's number (approximately 2.71828).

Example

print math::exp(0) # 1
print math::exp(1) # 2.718281828459045

expm1

fn expm1 num -> num

Description

Returns the exponential of the given number minus one. The exponential function is defined as e^x, where e is Euler's number (approximately 2.71828).

Example

print math::expm1(0) # 0
print math::expm1(1) # 1.718281828459045

ln

fn ln num -> num

Description

Returns the natural logarithm of the given number. The natural logarithm is the inverse of the exponential function.

Example

print math::ln(1) # 0
print math::ln(2.718281828459045) # 1

log

fn log num -> num

Description

Returns the base-10 logarithm of the given number.

Example

print math::log(1) # 0
print math::log(10) # 1

log10

fn log10 num -> num

Description

Returns the base-10 logarithm of the given number.

Example

print math::log10(1) # 0
print math::log10(10) # 1

log2

fn log2 num -> num

Description

Returns the base-2 logarithm of the given number.

Example

print math::log2(1) # 0
print math::log2(2) # 1

sqrt

fn sqrt num -> num

Description

Returns the square root of the given number.

Example

print math::sqrt(4) # 2
print math::sqrt(2) # 1.4142135623730951

cbrt

fn cbrt num -> num

Description

Returns the cube root of the given number.

Example

print math::cbrt(8) # 2
print math::cbrt(2) # 1.2599210498948732

hypot

fn hypot num, num -> num

Description

Returns the square root of the sum of the squares of the given numbers.

Example

print math::hypot(3, 4) # 5
print math::hypot(2, 2) # 2.8284271247461903

pow

fn pow num, num -> num

Description

Returns the first number raised to the power of the second number.

Example

print math::pow(2, 3) # 8
print math::pow(2, 0.5) # 1.4142135623730951

ceil

fn ceil num -> num

Description

Returns the smallest integer greater than or equal to the given number.

Example

print math::ceil(1.5) # 2
print math::ceil(-1.5) # -1

floor

fn floor num -> num

Description

Returns the largest integer less than or equal to the given number.

Example

print math::floor(1.5) # 1
print math::floor(-1.5) # -2

round

fn round num -> num

Description

Returns the nearest integer to the given number.

Example

print math::round(1.5) # 2
print math::round(-1.5) # -2

abs

fn abs num -> num

Description

Returns the absolute value of the given number.

Example

print math::abs(1) # 1
print math::abs(-1) # 1

NaN

NaN: num

Description

A special value representing "not a number".

Example

print math::NaN # NaN

Standard Library - Time Functions

now

fn now -> {
    year -> num,
    month -> num,
    day -> hour,
    hour -> num,
    minute -> num,
    second -> num
}

Description

Returns the current date and time as an object with the following fields:

  • year - The current year.
  • month - The current month.
  • day - The current day.
  • hour - The current hour.
  • minute - The current minute.
  • second - The current second.

Example

print now() # {year -> 2021, month: 9, day -> 1, hour -> 0, minute -> 0, second -> 0}

clock

fn clock -> num

Description

Returns the number of milliseconds since the Unix epoch.

Example

print clock() # 1686614986960

Standard Library - Type Functions

num

fn num any -> num

Description

Converts the given value to a number. Otherwise, returns math::NaN.

Example

print num("123") # 123
print num("abc") # NaN

str

fn str any -> str

Description

Converts the given value to a string.

Example

print str(123.4) # 123.4

type_of

fn type_of any -> str

Description

Returns the type of the given value as a string.

Example

print type_of(123) # "num"
print type_of("abc") # "str"

is?

fn is? any, str -> bool

Description

Returns true if the given value is of the given type, false otherwise.

Example

print is?(123, 'num) # true
print is?(123, 'str) # false

Standard Library - List Functions

len

fn len [any] -> num

Description

Returns the length of the given list.

Example

print len([1, 2, 3]) # 3

push

sub push [any] any

Description

Adds the given value to the end of the given list.

Example

list = [1, 2, 3]
push list, 4
print list # [1, 2, 3, 4]

pop

fn pop [any] -> any

Description

Removes and returns the last element of the given list.

Example

list = [1, 2, 3]
print pop(list) # 3
print list # [1, 2]

shift

fn shift [any] -> any

Description

Removes and returns the first element of the given list.

Example

list = [1, 2, 3]
print shift(list) # 1
print list # [2, 3]

unshift

sub unshift [any] any

Description

Adds the given value to the beginning of the given list.

Example

list = [1, 2, 3]
unshift list, 0
print list # [0, 1, 2, 3]

join

fn join [any], str -> str

Description

Returns a string containing the elements of the given list joined by the given separator.

Example

print join([1, 2, 3], ", ") # "1, 2, 3"

map

fn map [any], (fn any -> any) -> [any]

Description

Returns a new list containing the results of applying the given function to each element of the given list.

Example

print map([1, 2, 3], lambda x -> x * 2) # [2, 4, 6]

map!

sub map! [any], (fn any -> any)

Description

Applies the given function to each element of the given list.

Example

list = [1, 2, 3]
map! list, lambda x -> x * 2
print list # [2, 4, 6]

filter

fn filter [any], (fn any -> bool) -> [any]

Description

Returns a new list containing the elements of the given list for which the given function returns true.

Example

print filter([1, 2, 3], lambda x -> x % 2 == 0) # [2]

filter!

sub filter! [any], (fn any -> bool)

Description

Removes all elements from the given list for which the given function returns false.

Example

list = [1, 2, 3]
filter! list, lambda x -> x % 2 == 0
print list # [2]

reduce

fn reduce [any], any, (fn any any -> any) -> any

Description

Applies the given function to each element of the given list, accumulating the result.

Example

print reduce([1, 2, 3], 0, lambda x y -> x + y) # 6

upTo

fn upTo num -> [num]

Description

Returns a list of numbers from 0 up to the given number.

Example

print upTo(3) # [0, 1, 2, 3]

downTo

fn downTo num -> [num]

Description

Returns a list of numbers from the given number down to 0.

Example

print downTo(3) # [3, 2, 1, 0]

Standard Library - Map Functions

TODO

This page is a stub. It will be filled in later.

Standard Library - Misc Functions

execute

fn execute str... -> {
    wait_for -> (fn -> num)
}

Description

Executes the given command in a new process. Returns an object with a wait_for method that waits for the process to finish and returns its exit code.

Example

print execute("echo", "Hello, World!")::wait_for() # 0

times

sub times num, fn

Description

Calls the given function the given number of times.

Example

3.times do i
    print i
end

set_dir

sub set_dir str

Description

Sets the current working directory to the given path.

Example

set_dir "/home/user"

get_dir

fn get_dir -> str

Description

Returns the current working directory.

Example

print get_dir() # /home/user

in_dir

sub in_dir str, callable

Description

Sets the current working directory to the given path, calls the given callable, and then restores the previous working directory.

Example

set_dir "/foo/bar"
print get_dir() # /foo/bar

in_dir "/home/user" do
    print get_dir() # /home/user
end

print get_dir() # /foo/bar