Syntax

The fundamental syntax of the Talos programming language

This document describes the syntax design of the Talos programming language. Much of this is provisional until a version 1.0.0 can be produced.

The language is designed to be familiar, whilst also streamlining some elements to simplify readability, usability and internal development. For example, by purposefully designing the syntax, we can more efficiently parse programs (eg: types annotated after identifiers), or we can improve developer experience (eg: module file-names before the imports).

Comments

Line comments start with // and end at newlines.

// This is a comment.

Talos does not support block-comments, but instead features doc-comments that are denoted by //! instead.

//! This is a documentation comment.

Numerics

The following numeric literals are supported:

  • 12345 (decimal)
  • 0.1234 (floating-point)
  • 0xBEEF (hexadecimal)
  • 0o755 (octal)
  • 0b10101 (binary)
  • 5e+10 (scientific, +/- is optional)

Strings

Strings are supported as single-line and as blocks.

"example"; // single-line literal

Sequences

Within a string literal, the following escape sequences are recognized:

EscapeMeaning
\tHorizontal tab
\vVertical tab
\nNewline character
\rCarriage return

Interpolation

Talos does not yet explicitly allow string interpolation. This can however be achieved with format strings:

Debug.println("Hello, {}!".fmt("World"));

Format strings use the underlying fmt library syntax.

Tokens

The following is the current symbolic tokens/operators list recognized in Talos (in order from HIGHEST to LOWEST):

OperatorDescription
[]Typed Expressions
. ?. ()Field, Optional Chaining, Function Calls
asType Conversion Cast
- ! ~Negate, Logical Not, Complement, Increment, Decrement
**Exponentiation (Exponent)
* / %Multiply, Divide, Modulo (Factor)
+ -Addition, Subtraction (Term)
<< >>Left Shift, Right Shift
< > <= >=Comparison Operators
isType Comparson Test
== !=Equality Operators
&Bitwise AND
^Bitwise XOR
|Bitwise OR
&&Logical AND
|| ?:Logical OR, Nullish Coalescing
= ?/:Assignment, Ternary
,Comma Operator

Identifiers

Naming rules are similar to other programming languages. They may start with a valid letter, underscore or dollar sign and may contain letters, digits and underscores. Their casing is sensitive.

Some keywords cannot be used as identifiers and as such are considered reserved. These are:

let mut
in is as
import export
class public
protected private
fn return panic
type enum namespace
if else match
for loop break continue

Conventions

For idiomatic Talos code, these conventions are recommended:

ItemConvention
TypesUpperCamelCase
EnumsUpperCamelCase
ClassesUpperCamelCase
NamespacesUpperCamelCase
Boolean LiteralsUpperCamelCase
Void Type/LiteralUpperCamelCase
Keywordslower_snake_case
Variableslower_snake_case
Memberslower_snake_case
Self Literallower_snake_case

Variables

Variables declare a named value that can be used within Talos code. They are required to denote their immutability, and can optionally contain a type-annotation. If no typing is given, then their type is auto-inferred.

let x: Number = 42;     // Immutable, explicitely typed (Number).
mut y = x;              // Mutable, typing-inferred (Number).
let z: Any = y;         // Immutable, dynamicly typed.

Functions

Functions are a core structural unit within Talos. For example:

let add = fn (a: Number, b: Number): Number { return a + b; };
let sub = fn (a, b: Any) => a + b;

The first thing to note is that functions are values. As such, they must be explicitly bound to a variable. The reasoning for this is part of how Talos aims to be colorless in calling conventions and concurrency.

Breaking apart add we can see that:

  • fn is the keyword to introduce a function.
  • The parameters are contained within parenthesis ( and ).
  • It is annotated with the return-type Number.
  • Contains a body block of code. If the return-type was omitted, it is assumed to be Void.
  • Requires a closing semicolon since it is a value, not a declaration.

Moving to the sub declaration we can further see that:

  • Parameter a has no typing. Thus it inferred as Any.
  • The code block is instead replaced with an arrow statement.
  • The return-typing is auto-inferred from the arrow result as Any.

Note: The code-statement used for functions decides the type-inferrence of return values.

Flow

Control flow consists of the sequence in which statements are executed. They usually define a scope (enclosed by { and }) and are terminated by a semicolon.

Conditionals

The if and else statements provide conditional execution of blocks. These are constructed similarly to other C-like languages.

if (initial_condition) { ... }
else if (other_condition) { ... }

Loops

There are two available loop statements, loop and for.

The loop statement can be used to construct finite/infinite loops. This is dependent on the condition, if given.

loop (condition) { ... }    // similar to a while loop
loop { ... }                // auto-inferred as infinite

The for statement supports range-based looping over iterables.

for (ch, ii in "abc") Debug.println(ii, ch);

The utility statements break and continue can be used for additional control over loop statements.

Return

The return statement ends the flow of execution within a function, returning execution to the caller. A value can optionally be provided (dependent on the function context).

let sign = fn (n: Number): Number {
  if (i > 0) return 1;
  if (i < 0) return -1;
  return 0; // no-sign
}

Panic

Although usable as an expression, the panic operator throws an immediate exception.

Matches

A match statement mirrors that of the switch statement in other languages, however it also allows for additional case patterns.

let value = "abc";
let guard = (value) => value == "ab";

match (value) {
    "a": Debug.println("Matches exactly 'a'");
    String: Debug.println("Value is a string");
    Number: Debug.println("Value is a number");
    guard: Debug.println("Matched guard function");
    *: Debug.println("Did not match anything")
};

Unlike other switch statements, Talos' match statement allows checking values against their runtime types (eg: String), or through a guard function.

Classes

A class in Talos is a user-defined record type. All fields are referenced by their names and classes are the primary mechanism for users to construct more complex typings. Talos supports both named - "nominal" and anonymous - "structural", class types. Nominal classes are all distinct, however structural types are equal if they possess the same fields/types.

Nominal

A declaration for a nominal class type could be:

class Position {
    public let x: Number = 0;
    public let y: Number = 0;
};

Where breaking apart Position gets us:

  • A class introducer
  • The name of the class (optional)
  • A block containing field declarations
  • Declarations are defaulted to private

Constructors

Classes can be annotated with a limited function signature to denote their construction.

// Defines a class with constructor arguments.
class Position(a_x?: Number, a_y?: Number) {
    public let x = a_x ?? 0;
    public let y = a_y ?? 0;
};

Inheritance

For inheritance, we can have a class that extends another. This gives us the following declaration:

class Base(_: Any) {};
class Derived => Base("...") {};

As can also be seen, we can pass the arguments from the child constructor to the parent.

Structural

Classes can also inherit from structural types.

type Labeled = {
    label: String;
};

// Enforce that these classes must have a label.
class Derived(a_label: String) implements Labeled {
    public let label = a_label;
};

// Prepare an anonymous object with the "Labeled" interface.
let other: Labeled = {
    let label = "other";
};

// Declare a printer to test all incoming items.
let printer = fn (item: Labeled) => Debug.println(item.label);

printer(other);                 // Valid!
printer(Derived("another"));    // Valid!

Note: Although structural types are powerful, they are only accessibly within the type-world. As such, they cannot be used with the as or is operators.

Attributes

During compile-time, there are some additional properties that a developer may want to declare. To do this, Talos introduces attributes with the #[] syntax above a declaration.

This allows us to alter certain properties of values. The current attributes available are:

AttributeExplanation
ClassAllows altering compile-time class properties
OperatorAllows defining operators for Object values
OverloadAllows overloading function signatures
DeprecatedEmits deprecation messages as diagnostics

Decorators

Alternatively to attributes are decorators. These instead are applied at runtime, and although powerful, have a more restricted scoping for outputs.

Namespaces

Similar to TypeScript, Talos allows wrapping variables within a parent namespace.

namespace Validation {
    // Note: To access variables outside of the namespace
    // scoping, they must be declared as an export.
    export let check = fn (value: Any) { ... };
};

Modules

Talos includes a file-based module system for multi-file programs. Variables can be exposed to other files by declaring them via the export keyword.

// File: "a.tal"
export let value = 42;

To then access value from another file, it can be imported with the import keyword.

// File: "b.tal"
import "a.tal" as { value };
Debug.println("V:", value);

Imports can either destructure the incoming exports, or rename them entirely.

import "a.tal" as A;

Since import paths are internally contained as URLs, this gives Talos the best option for exposing internal and external crates.

Schemes

SchemeExplanation
<empty>Defaults to file
fileFilesystem import
talosTalos library crates
dylibTalos dynamic crates
crateUser installed crates

Crates

To accomodate complex internal/external dependencies, Talos implements this similarly to other languages by exposing libraries with a _crate.jsonc file. For more information, see the configuration guide.

On this page