The Aria Programming Language

Welcome to Aria!


Project maintained by egranata Hosted on GitHub Pages — Theme by mattgraham

Aria Style Guide

1. Introduction

This document provides a set of style and coding conventions for writing Aria code. The goal is to encourage code that is readable, predictable, and consistent across the entire project. Adhering to these guidelines will make the codebase easier to understand, maintain, and extend.

This guide is based on the conventions observed in the official Aria standard library and test suite.

2. Naming Conventions

Consistency in naming is critical for readability. Use the following conventions for different identifiers.

2.1. General Naming Rules

Identifier Type Convention Example(s)
Modules / Files snake_case.aria json_parser.aria, http_request.aria
Variables snake_case val user_data = ..., val request_url = ...
Functions snake_case func calculate_hash(key) { ... }
Structs PascalCase struct JsonStream { ... }
Enums PascalCase enum TaskStatus { ... }
Enum Cases PascalCase case InProgress, case Completed
Mixins PascalCase mixin Iterable { ... }
Constants UPPER_SNAKE_CASE val SECONDS_PER_MINUTE = 60;

2.2. File Names

Aria source files should end with the .aria extension and be named using snake_case.

Good: map.aria, file_utils.aria Bad: Map.aria, file-utils.aria

2.3. Type Names

Structs, enums, and mixins should be named using PascalCase.

struct RequestClient { ... }

enum RequestStatus { ... }

mixin Loggable { ... }

2.4. Function and Variable Names

Functions and variables should be named using snake_case.

func fetch_user_profile(user_id) {
    val profile_url = "https://example.com/api/users/{0}".format(user_id);
    # ...
}

3. Formatting

A consistent formatting style is essential for code that is easy to read and visually parse. Note that there is no automated formatter for Aria code, and these guidelines should not be read as strict rules, until such a formatter is made available.

3.1. Indentation

Use 4 spaces for indentation. Do not use tabs.

3.2. Braces

The opening brace { should be on the same line as the declaration, separated by a space. The closing brace } should be on its own line, aligned with the start of the declaration.

# Good
struct MyStruct {
    func do_something() {
        if condition {
            # ...
        } else {
            # ...
        }
    }
}

# Bad: Opening brace on new line
struct MyStruct
{
    # ...
}

3.3. Spacing

# Good
val result = (x + y) * z;
val items = [1, 2, 3];
func my_func(arg1, arg2) { ... }
my_func(1, 2);

# Bad
val result=(x+y)*z;
val items = [1,2,3];
func my_func (arg1, arg2) { ... }

3.4. Line Length

Keep lines under 100 characters where possible to ensure readability.

3.5. Blank Lines

Use a single blank line to separate top-level function, struct, enum, or mixin definitions.

Within functions, use blank lines sparingly to group related statements into logical blocks.

4. Comments

Comments should explain why code does something, not what it does. The code itself should be clear enough to explain the “what”.

# Good: Explains the reason for the check.
# The remote API returns a special value for legacy users.
if user.is_legacy {
    # ...
}

5. File and Code Organization

Aria files should have a consistent structure to make them easy to navigate.

5.1. File Structure

Organize the contents of a .aria file in the following order:

  1. License Header: All files in the Aria standard library and test suite must begin with the SPDX license identifier.
    # SPDX-License-Identifier: Apache-2.0
    
  2. File Flags (Optional): If the file requires special handling by the VM or build system, the flag directive comes next.
    flag: no_std;
    flag: uses_dylib("aria_http");
    
  3. Import Statements: All import statements follow.
  4. Helper Functions: Free-standing helper functions that are used by the main types in the file.
  5. Type Definitions: The core struct, enum, or mixin definitions of the module. Multiple types per module may be defined, as long as they are semantically correlated. For example, the iterator for a type is generally defined in the same module as the type.
  6. Extensions: extension blocks that add functionality to the types. Multiple extension blocks may be defined in a single module, as long as they are semantically correlated. extensions may be used to split a type definition in multiple chunks, if the type is sufficiently large. Individual chunks should maintain their own logical grouping and possibly be ordered in dependency order.

5.2. Imports

5.3. Documentation

Follow the existing documentation style and conventions as the existing standard library documentation.

6. Language Features Best Practices

6.1. Functions

Argument Order

Function arguments must be ordered as follows:

  1. Required arguments.
  2. Optional arguments (with default values).
  3. Variable arguments (...).
func process_data(item, retries=3, ...) {
    # ...
}

Type Checking

For public APIs and complex functions, use type hints to improve clarity and documentation.

func new_with_capacity(n: Int) {
    # ...
}

Use isa checks when type hints are not available or you need to discriminate between different possible types. Do not compare types directly. Throw RuntimeError::UnexpectedType only if receiving an object of an unsupported type is truly not expected by the API contract.

One-line functions

For functions that are one single return statement, you may use the one-line function syntax


# Good
func add(x,y) = x + y;

# Good
func add(x,y) = {
    return x + y;
};

Do not use the one-line syntax for complex or multi-line expressions


# Good
func gcd(a,b) {
    if a == 0 {
        return b;
    }
    return gcd(b % a, a);
}

# Bad
func gcd(a,b) = a == 0 ? b : gcd(b % a, a);

6.2. Structs

struct Map {
    type func new() {
        return Map.new_with_capacity(128);
    }

    type func new_with_capacity(n: Int) {
        return alloc(This){
            .capacity = n,
            # ...
        };
    }

    func prettyprint() {
        return "Map(...)";
    }
}

append and remove may also be called push and pop respectively, if the collection is stack-like.

6.3. Enums

enum WebEvent {
    struct PageLoad { url: String }
    struct Click { x: Int, y: Int }

    case Load(WebEvent.PageLoad),
    case Click(WebEvent.Click),
    case KeyPress(String)
}

6.4. Operator Overloading

struct Complex {
    # ...
    operator +(rhs) {
        if (rhs isa Int) || (rhs isa Float) {
            return Complex.new(this.real + rhs, this.imag);
        } elsif rhs isa Complex {
            return Complex.new(this.real + rhs.real, this.imag + rhs.imag);
        } else {
            throw alloc(Unimplemented);
        }
    }

    reverse operator +(lhs) {
        return this._op_impl_add(lhs);
    }
}

6.5. Iterators

The standard library follows a consistent pattern for iteration that should be adopted in user code.

import Iterator, Iterable from aria.iterator.mixin;

struct MyCollection {
    # ...
    func iterator() {
        return MyCollectionIterator.new(this);
    }
    include Iterable
}

struct MyCollectionIterator {
    # ...
    func next() {
        if finished {
            return Maybe::None;
        }
        return Maybe::Some(next_item);
    }

    include Iterator
}

6.6. Error Handling

Follow these principles for robust error handling:

  1. For expected absence of a value, return a Maybe. This is for non-error conditions, like a key not being found in a map. The caller is expected to handle Maybe::None.

    # Good: Key might not exist, which is not an error.
    func get_from_cache(key) {
        if cache.contains(key) {
            return Maybe::Some(cache[key]);
        } else {
            return Maybe::None;
        }
    }
    
  2. For expected failure of an operation, return a Result. This is for expected conditions, like attempting to read a file. The caller is expected to handle Result::Err.

    # Good: File might not exist, might not be readable, ...
    func read_config() {
        if !config_path.exists() {
            return Result::Err(FileReadError.new("Configuration file not found at {0}".format(config_path)));
        }
        if !config_path.readable() {
            return Result::Err(FileReadError.new("Configuration file not readable at {0}".format(config_path)));
        }
        return Result::Ok(config_path.read());
    }
    

For the purposes of this sample code, of course, ignore time-of-check/time-of-use issues.

  1. For recoverable errors, throw an exception. This is for situations that are erroneous but potentially recoverable by an upstream caller, such as a transient failure. Define custom structs or enums for your exceptions.

    struct ExpiredCertificate { ... }
    
    func validate_certificate(path) {
        if certificate_is_expired(path) {
            throw ExpiredCertificate.new("Certificate has expired at {0}".format(path));
        }
        # ...
    }
    

Guidelines for exceptions:


# Good
struct WhatATerribleFailure {
    type func new(msg: String) {
        return alloc(This) {.msg = msg};
    }

    func prettyprint() {
        return "what a terrible failure: {0}".format(this.msg);
    }
}

# Good
enum TerribleFailures {
    case RemoteHostPanic(String),
    case PasswordFileNotFound(String),
    # ...

    func prettyprint() {
        match this {
            # ...
        }
    }
}

# Bad
struct RemoteHostPanic {
    type func new(msg: String) {
        return alloc(This) {.msg = msg};
    }

    func prettyprint() {
        return "remote host panic: {0}".format(this.msg);
    }
}

struct PasswordFileNotFound {
    type func new(msg: String) {
        return alloc(This) {.msg = msg};
    }

    func prettyprint() {
        return "password file not found: {0}".format(this.msg);
    }
}
  1. For irrecoverable errors, assert the condition. Prefer exceptions or error returns in library code as assert is non-recoverable for the user. In program code, assert liberally. In library code, assert sparingly and only to uphold invariants that would lead to corrupted state or operation if violated.

6.7 match statements