Orbis

Orbis will be a new-gen language, it will prioritize human readability while ensuring computational efficiency. It will be:

  • Native compiled (initially to x86 and ARM)
  • Modern
  • Adaptable learning curve
  • Based on the DRY (Don't Repeat Yourself) principle
  • Fast to iterate

Inspiration

Every human creation is inspired by others, and Orbis draws inspiration from several programming languages:

  • C/C++
  • Rust
  • Zig
  • Nim
  • Odin
  • Go/V
  • C#
  • Kotlin/Scala
  • Elixir/Gleam
  • TypeScript

Yes, that is a lot of languages, and some were left out. Orbis will learn the lessons from them.

Goals

  1. A language that can be applied in a, but not limited to, high-performance, critical computing
  2. A language that feels natural to write and read with an adaptable learning curve
  3. A language that can be extended (see macros and meta-programming)
  4. A language that can be learned in less than an hour for a programmer

Features

  • Static typing
  • Type inference
  • Templates/Generics/Macros/Meta-programming in general
  • Memory safety (like a borrow checker)
  • Concurrency
  • No try/catch, only Result and Option types
  • Pattern matching
  • Extensible via 'extensions'
  • C/C++ interoperability out of the box
  • Standard library with the most common needs

About this

This guide is a work in progress, currently in v0.1. Think it as a small specification. Things may change, be removed or added and here is a list of no done things:

  • Strings
  • Memory management
  • Standard library
  • Lifetime management
  • SoA and AoS
  • Macros and meta-programming
  • Annotations or attributes
  • Reflection
  • Functional programming
  • Async / Await
  • Operator overloading

The original guide was written in about 8 hours, so may have some typos and errors. If you find one, please open an issue. Also if you have any suggestions, questions or want to contribute, please open an issue or a PR.

Layers

Like onions (or ogres), Orbis is separated into layers. Each layer is a different part of the language, and they are all connected to each other. The layers are:

  • Language: The core of the language, where the syntax and semantics are defined.
  • Core: A basic set of utilities and functions that are always available. You can disable them if you want.
  • Standard Library: A set of modules that provide common functionality, like math, I/O, and collections.
  • Extensions: Additional modules that provide more functionality. You can enable or disable them as you wish.

This separation allows you to have a minimal language if you want, or a full-featured one. You can also create your own modules and use them in your projects.
It's important to know about the existence of these layers, because they can help you understand how the language works and how you can customize it.

Syntax

It will be based in the C-Syntax with some Rust, Kotlin and Zig influences.

Move and copy semantics

The main difference with other langues will be the 'move semantics'. The = operator copies values, while the <- operator moves values. For example:

mut let a = 10; // copy
let b <- 20;    // move (same as let b = 10; but with move semantics)
mut let c: u32; // can define a variable without initialize it (it will be 0 in this case)

c = a;          // a is copied to c, a is still valid
a <- b;         // a = 10, b is dropped


io.print(a); // 20
io.print(c); // 10
io.print(b); // compile error! b is dropped

By default any variable declared will be immutable, you need to add the mut keyword to make it mutable. Also, any variable will be copied by default, unless you use the <- operator or force a move.

Scopes and lifetimes

The language will have a 'scope-based' memory management, when a scope ends, all variables in the scope will be dropped.

let a = 10;

{
    // a is in scope
    let a = 30;     // compile error! a is already defined
    let b = 20;
    io.print(b);    // 20
    // b will be dropped here
}

io.print(a); // 10
io.print(b); // compile error! b is out of scope

A scope always return a value; the last expression within the scope will determine this value.

let a = {
    let b = 10;
    let c = 20;

    b + c
};  // a: u32 = 30

Comments

There will be two types of comments in the code:

// Single line comment and

/*
    Multi-line comment
*/

Multi-line comments can be used as documentation:

/*
    This function adds two numbers together.
    @param a The first number
    @param b The second number
    @return The sum of a and b
*/
fn add(a: i32, b: i32): i32 {
    a + b
}

Native types

Orbis will have some basic types out-of-the-box, the integer types are:

TypeLength (in bytes)C equivalentNote
u81uint8_tUnsigned 8-bit
i81int8_tSigned 8-bit
u162uint16_tUnsigned 16-bit
i162int16_tSigned 16-bit
u324uint32_tUnsigned 32-bit
i324int32_tSigned 32-bit
u648uint64_tUnsigned 64-bit
i648int64_tSigned 64-bit
u12816uint128_tUnsigned 128-bit
i12816int128_tSigned 128-bit
u25632uint256_tUnsigned 256-bit
i25632int256_tSigned 256-bit

The floating-point are based on IEEE 754:

TypeLength (in bytes)C equivalentNote
f162?16-bit floating-point
f324float32-bit floating-point
f648double64-bit floating-point
f12816?128-bit floating-point
f25632?256-bit floating-point (may be removed)

Chars in the other hand will be implemented as aliases of:

TypeLength (in bytes)C equivalentNote
c81charSee u8 type (ASCII)
c162char16_tSee u16 type (UTF-16)
c324char32_tSee u32 type (UTF-32)

Strings will not be a native type, they will be more like a 'composite' type, more on this later.

[!WARNING] Define how the Strings will be implemented

Booleans will be a native type, but they will more like a 'bit-set'. Later will be explained. Important to know, you can define your own types or aliases to the native types.

type Int = i32;
type Float = f32;

Initialization values

All variables are initialized with the 'initial value' of their type, for example:

let a: i32; // 0
let b: f32; // 0.0
let c: b8;  // false or 0

Bit-sets

A bit-set will be a type that can store a arbitrary number of bits, for example:

let a: b8;  // 8 bits or traditional 'bool'
let b: b16; // 16 bits
let c: b3;  // 3 bits

This allow a easy way to store flags and other bit-based data, also provides a good way to create padding in structs or classes, using the least amount of memory possible.

Operators

Orbis will provide a set of operators in the language itself, you can define your own operators or overload the existing ones.

OperatorDescription
+Addition
-Subtraction
*Multiplication
/Division
%Modulus
&Bitwise AND
|Bitwise OR
^Bitwise XOR
~Bitwise NOT
<<Bitwise left shift
>>Bitwise right shift
&&Logical AND
||Logical OR
!Logical NOT
==Equal
!=Not equal
<Less than
<=Less than or equal
>Greater than
>=Greater than or equal
&Reference
<-Move
?Optional / Null check
!!Null panic
|>Pipe operator

More operators may be added.

Saturating and Wrapping Operators

Also, Like in Zig it will have 'saturating' operators and 'wrapping' operators.

let a = 255;

let b = a + 1;  // compile error!
let c = a %+ 1; // ok, c = 0
let d = a |+ 1; // ok, d = 255

Think it as:

  • %+ executes a + and then a % operation
  • |+ executes a if and, if isn't at the maximum value, executes a + operation

Pipe Operator

The |> operator is used to pipe the result of an expression to a function.

fn double(a: u32) -> u32 {
    return a * 2;
}

fn square(a: u32) -> u32 {
    return a * a;
}

let a = 10;

let b = a |> double |> square; // square(double(a))

It's intended to be used as a way to chain functions, like in Elixir.

Null, Optionals and Undefined

Null and Optionals

[!WARNING] null may be removed, also 'Optionals' can become part of the 'core' layer instead of the language itself.

null is a special value in many languages that represents the absence of a value. This can lead to many issues, such as null pointer exceptions (Java and C#). In Orbis, null is not a special value, but rather a type. This means that you can't assign null to a variable unless it is explicitly typed as null.

mut let a: u32?; // a is an optional u32, by default it is null
io.print(a); // compiler error! a is null

a = 10;

if (a) {
    io.print(a); // ok, a = 10
}

io.print(a); // also ok, a = 10

a = null; // ok, you can assign null to a

To reduce the code verbosity, you can use the ? operator to check if an optional is null or not.

mut let a: s8? = "Hello";

if (a?.length() > 0) { // only executes if a is not null and has a length greater than 0
    io.print("a is not null and has a length greater than 0");
} else {
    io.print("a is null or has a length of 0");
}

Undefined

By default, all variables have an 'initial value', even if you don't assign one. undefined is a special value that works like C and C++'s utilized variables. You can't use the variable until you assign a value to it.

mut let a: u32 = undefined; // a is undefined, it's a garbage value
io.print(a); // compiler error! a is undefined
a = 10;
io.print(a); // ok, a = 10

Remember that undefined is not the same as null. undefined is a garbage value, while null is a special value that represents the absence of a value. Also, don't abuse undefined in your code, it's present only for compatibility with C and C++.

Null Panics

If you want to panic when an optional is null you can use the !! operator.

mut let a: u32? = null;

io.print(a!!); // valid, panics because a is null

Use this operator with caution, as it can lead to runtime errors if not used properly.

Control flow

Orbis will have: if, for and do-for loops.

let a = 10;
if (a == 10) {
    io.print("a is 10");
} else {
    io.print("a is not 10");
}

let b = if (a == 10) 20 else 30;    // ternary operator are replaced by inline if

for (let i = 0; i < 10; i++) {
    io.print(i);
}

let k = 0;
do {
    io.print(k);
} for (k < 10; k++);   // do-for loop, initial value is optional

Defer

defer is a statement that allows you to execute a block or line of code when the current scope is exited. This is useful for cleaning up resources, such as closing files or freeing memory.

let file = io.open("file.txt", io.Mode.Read);
defer io.close(file);

// do something with the file

The defer statement will execute the code in the reverse order that it was declared.

defer {
    io.print("world");
    io.print("hello");
}

// Output: hello world

You can defer based on a condition:

let a = 5;
defer if a > 0 {
    io.print("a is greater than 0");
}

Functions

Functions will be first-class citizens, you can pass them as arguments, return them and store them in variables.

fn add(a: i32, b: i32): i32 {   // a and b are copied (see last line)
    return a + b;               // return is a implicit move
}

let a = 10;
let b = 20;

let c = add(a, b);  // copied a and b

io.print(a);        // 10
io.print(b);        // 20
io.print(c);        // 30

Also you can move values to functions:

fn add(a: i32, b: i32): i32 { // a and b are moved (see last line)
    return a + b;               // return is a implicit move
}

let a = 10;
let b = 20;

let c = add(<-a, <-b);  // moved a and b

io.print(a);    // compile error! a is moved
io.print(b);    // compile error! b is moved
io.print(c);    // 30

Other feature is that you can force a move value:

fn add(a: i32, @move b: i32): i32 {
    return a + b;
}

let a = 10;
let b = 20;

add(a, b);      // compile error! b need to be moved
add(a, <-b);    // forced to move b

User Defined Types

All user defined types are immutable by default, you can add the mut keyword to make it mutable. The only type that is part of the language is the struct, all other types are 'syntax sugar' for them.

Structs

Structs will be the main way to create types, they will be similar to C or Rust structs.

struct Point {
    x: f32; // public, can't be changed
    y: f32;
}

Struct are only immutable data, you can add the mut keyword to make it mutable:

struct Point {
    mut x: f32;
    mut y: f32;
}

Like in Go, you can create a method for a struct:

struct Point {
    x: f32;
    y: f32;
}

fn (p: Point) distance(other: Point): f32 {
    return Math.sqrt((p.x - other.x) * (p.x - other.x) + (p.y - other.y) * (p.y - other.y));
}

To enable some kind of inheritance, and to not use composition, you can use the use keyword:

struct Point {
    x: f32;
    y: f32;
}

struct Point3D {
    use Point; // x and y will be in Point3D
    z: f32;
}

let p = Point3D { x: 1.0, y: 2.0, z: 3.0 };

For better understanding, the use keyword is similar to the extends keyword in other languages. The compiler will copy the fields and methods from the Point struct to the Point3D struct.

Classes

Classes are similar to structs, but they can have methods defined inside them. They will be similar to C++ classes.

[!WARNING] Classes may be removed in the future, because they are a 'syntax sugar' for composites. They currently exist to facilitate C++ compatibility. Also, inheritance may be changed in favor of composition and use.

class Point {
    x: f32; // private and constant by default
    y: f32;

    pub myValue: f32;   // public
    sub myValue2: f32;  // sub is like protected in other languages

    pub fn constructor(x: f32, y: f32) {
        this.x = x;
        this.y = y;
    }

    pub fn distance(other: Point): f32 {
        return sqrt((this.x - other.x) * (this.x - other.x) + (this.y - other.y) * (this.y - other.y));
    }
}

By default all classes have the 'constructor' and 'destructor' traits. If you want to modify the value of a class you will need to use the 'mut' keyword:

class Point {
    mut x: f32;
    mut y: f32;

    pub fn constructor(x: f32, y: f32) {
        this.x = x;
        this.y = y;
    }

    pub fn move(x: f32, y: f32) {
        this.x = x;
        this.y = y;
    }
}

Although inheritance is supported, the language encourages composition as the primary design approach.

class Rectangle {
    x: f32;
    y: f32;
    width: f32;
    height: f32;

    pub fn constructor(x: f32, y: f32, width: f32, height: f32) {
        this.x = x;
        this.y = y;
        this.width = width;
        this.height = height;
    }
}

class Square: Rectangle {
    pub fn constructor(x: f32, y: f32, size: f32) {
        super(x, y, size, size);
    }
}

Important to note: classes are defined as 'macros' in the 'core' layer, and are not part of the language itself.

Composites

Composites are a way to define a 'class' that is composed by other classes.

[!WARNING] Composites may be unified with classes, with the keyword 'use'

class Drawable {
    color: Color;

    pub fn draw();
}

composite Shape: Drawable {
    // color is inherited from Drawable, but not in the traditional way
    x: f32;
    y: f32;

    pub fn constructor(color: Color, x: f32, y: f32) {
        // super(color); // compile error! composite can't call super because isn't have a 'super'
        this.color = color;
        this.x = x;
        this.y = y;
    }
}

For better understanding, the compiler will generate the following code:

composite Shape {
    color: Color;   // inherited from Drawable
    x: f32;
    y: f32;

    pub fn constructor(color: Color, x: f32, y: f32) {
        this.color = color;
        this.x = x;
        this.y = y;
    }

    pub fn draw();
}

Objects

[!WARNING] Objects may be removed in the future, because they are a 'syntax sugar' for global functions and variables.

Objects are like to 'singleton' in other languages. Objects can have methods and fields.

object Math {
    pub fn abs(a: i32): i32 {
        if a < 0 {
            return -a;
        }

        return a;
    }
}

Math.abs(-10);  // 10

Traits

Traits are a way to define a set of methods that a struct or class must implement. They are similar to interfaces in Java or C# or traits in Rust.

[!WARNING] Expand this section

trait Drawable {
    pub fn draw();
}

class Shape {
    x: f32;
    y: f32;

    pub fn draw() {
        // draw the shape
        // now the Shape class can be used as a Drawable
    }
}

Atoms and Enums

Atoms

[!WARNING] Atoms may be renamed to Symbols in the future.

Atoms are a way to define a unique value, they are similar to Erlang atoms, but they act more like a 'tag' than a 'value'.

let a = :atom1;
let b = :atom2;

if a == b {
    // this will never be executed
}

Atoms can't be compared with numbers but internally they will be a unique 64-bit value.

Enums

Based on atoms, enums are a way to define a set of atoms.

enum Color {
    Red,
    Green,
    Blue
}

let a = Color:Red;

Enums can have values:

enum Color {
    Red = 0xFF0000,
    Green = 0x00FF00,
    Blue = 0x0000FF
}

Or even can be more complex:

enum Color {
    RGB(r: b8, g: b8, b: b8),
    CMYK(c: b8, m: b8, y: b8, k: b8)
}

let a = Color:RGB(255, 0, 0);

Pattern matching

Pattern matching is a way to match values with patterns, it is similar to switch-case in C, but more powerful.

enum Color {
    Red,
    Green,
    Blue
}

fn color_to_string(color: Color): string {
    match color {
        Color:Red -> "Red",
        Color:Green -> "Green",
        Color:Blue -> "Blue"
    }
}

Pattern matching can be used with structs and classes:

struct Point {
    x: f32;
    y: f32;
}

fn point_to_string(point: Point): string {
    match point {
        Point(x, y) -> "Point(" + x + ", " + y + ")"
        Point(x, _) -> "Point(" + x + ", ?)"
        Point(0, 0) -> "Origin"
    }
}

Modules

Modules are the way to: organize the code, avoid name conflicts, and create a 'namespace'.

export module math {    // export is needed if you want to use the module outside the file
    pub fn abs(a: i32): i32 {
        if a < 0 {
            return -a;
        }

        return a;
    }
} 

Unexported variables, functions, etc. cannot be used outside the file. This is useful to create 'private' functions or variables without the need to create a class.

Const and compile-time evaluation

An important point before introducing more advanced features is the const keyword, that will be used to define compile-time values.

const ALWAYS_IN_SCREAMING_SNAKE_CASE = 10;
let a = ALWAYS_IN_SCREAMING_SNAKE_CASE;     // a = 10
let b <- ALWAYS_IN_SCREAMING_SNAKE_CASE;    // compile error! can't move a const value
const ALWAYS_IN_SCREAMING_SNAKE_CASE = 2;   // compile error! can't re-define a const value

The const keyword will be used to define compile-time values, that can be used in the code, for example:

const PI = 3.1416;

const fn area_of_circle(radius: f32): f32 {
    return PI * radius * radius;
}

let my_circle = area_of_circle(10); // 314.16, calculated at compile-time

With the const keyword you can't:

  • const a class or struct
  • const a function that have side-effects
  • const a function that have a return value that can't be evaluated at compile-time
  • const a value that can't be evaluated at compile-time
  • const a function that move values

Memory

Orbis will separate memory into two categories: high-level and low-level memory.

![WARNING] Some concepts are not yet defined, this is a work in progress.

  • High Level Memory: Collection of functions and types that abstract the memory operations, they are part of the 'core' layer. This is the default way to handle memory in Orbis.
  • Low Level Memory: Direct access to memory, this is the way to handle memory in a more 'C' way. Use this only when you need to.

Is important to note that all declared variables are allocated in the stack, to use the heap you need to explicitly allocate it.

High Level Memory

[!CAUTION] This is under construction, some features may be removed or added

In computers memory has only two possible methods: read and write. From here, we can derive all the other memory operations, such as:

  • Copy (one read, one write)
  • Move (one read, two writes)
  • Compare (two reads)
  • Swap (two reads, two writes)

From this are derived the 'copy' and 'move' semantics along other memory operations in Orbis.

References

References are a pointer to a values, think of them as a 'const' pointer in C++.

let a = 10;
let b = Ref(a); // reference to a

io.print(b); // 10

Rc and Owner

Rc and Owner are way to handle heap memory, they are similar to C++'s shared_ptr and unique_ptr. Rc implements a reference counter, as shown in the following example:

struct Point {
    x: f32;
    y: f32;
}

let point = Rc(Point(10, 20));  // point is now constructed in the heap with a reference count of 1

{
    let point2 = point;             // point2 is now a reference to point, the reference count is now 2
    io.print(point2.x); // 10
    // point2 is dropped, the reference count is now 1
}

io.print(point.x);  // 10
// point is dropped, the reference count is now 0, the memory is freed

In the other hand, Owner is a unique owner of the memory:

struct Point {
    x: f32;
    y: f32;
}

let point = Owner(Point(10, 20));  // point is now constructed in the heap with a reference count of 1

{
    let point2 = point;             // compile error! point is a unique owner
    let point3 <- point;            // point is moved to point3, is now the owner of the memory
    io.print(point3.x); // 10
    // point3 is dropped, the memory is freed
}

io.print(point.x);  // compile error! point is dropped

Low level memory

![WARNING] This is a work in progress, expand this section with more information.

Orbis will have a way to manipulate memory in a low level like C, but always the best practice is to use the before mentioned methods.

Pointers

Pointers are a way to access memory directly, they are a reference to a memory address.

let a = 10;
let b = &a; // b is a pointer to a

io.print(*b); // 10
io.print(b);  // some memory address

The & operator is used to get the memory address of a variable, and the * operator is used to get the value of the memory address. The type of a pointer is *T, where T is the type of the variable.

Pointer arithmetic

Pointer arithmetic is a way to move the pointer to another memory address. To allow this you need to use the unsafe keyword.

let a = 10;
mut let b = &a; // b is a pointer to a

unsafe {
    b += 1; // move the pointer to the next memory address
}

io.print(*b); // some random value

Error handling

Inspired by Rust and Kotlin, Orbis will use the 'Result' and 'Option' types to handle errors.

fn divide(a: i32, b: i32): Result<i32, string> {
    if b == 0 {
        return Err("Division by zero");
    }

    return Ok(a / b);
}

let a = divide(10, 2); // Ok(5)
let b = divide(10, 0); // Err("Division by zero")

You can also use atoms to handle errors:

fn divide(a: i32, b: i32): Result<i32, atom> {
    if b == 0 {
        return Err(:DivisionByZero);
    }

    return Ok(a / b);
}

Option is a way to handle optional values:

fn get_value(a: i32): Option<i32> {
    if a == 0 {
        return None;
    }

    return Some(a);
}

Attributes

[!CAUTION] This is under construction, name and syntax may change

Attributes are a way to add metadata to the code, they are similar to C# attributes or Rust's attributes.

@deprecated("Use the new function")
fn old_function() {
    // old code
}

Attributes can be used as a compiler hint, developer documentation, add metadata to the code (like __attribute__ in GCC) or even manipulate the code.

Generics

[!WARNING] This is under construction, name and syntax may change

Templates are a way to define a function or a struct that can be used with different types. They are compile-time evaluated.

fn add<T>(a: T, b: T): T {
    return a + b;
}

let a = add(10, 20);    // 30
let b = add(9.5, 20.5); // 30.0

let c = add(10.0, 20);          // compile error! can't add a f32 with a i32 (type mismatch)
let d = add<f32>(10.0, 20.0);   // compile error! can't return a f32 as a u32

Also, they can infer the type when it's possible:

struct Point {
    x: f32;
    y: f32;
}
let a: Rc<Point> = Rc(Point(10, 20)); 
// is the same as
let b = Rc(Point(10, 20));

Meta-programming

[!CAUTION] Research is needed to define how macros will be implemented

Extensions

Extensions are a way to incorporate (or even eliminate) new features to the language via macros. By definition only exist three types of extensions: 'superset', 'subset' and 'behavior'.

  • Superset: Add new features to the language like new types, operators, keywords, etc.
  • Subset: Opposite to 'superset', remove features from the language.
  • Behavior: Don't add or remove features, but change the behavior of the language.

Currently there are two proposed extensions: 'GPU' and 'Reactive'.

Reactive Extension

This extension will add the 'reactive programming' to the language, it will be based on the 'Observer' pattern. To enable reactive extension you need to: pass the '-extension=reactive' flag to the compiler and start the variables or functions with '$'. Also you can use the '@reactive' attribute.

@reactive
fn add(a: i32, b: i32): i32 {
    return a + b;
}

mut let $a = 10;
mut let $b = 20;

let $c = add($a, $b);   // $c = 30
$a = 20;                // $c = 40

GPU Extension

The purpose of this extension. is to enable the compilation of the code to run in a GPU. To enable GPU extension you need to: pass the '-extension=gpu' flag to the compiler. This will:

  • Remove the standard library
  • Remove bit-sets, 128-bit and larger types
  • Remove classes, composites, objects and traits
  • Remove pattern matching
  • Import by default the 'math' module
  • Import by default the 'gpu' module

Tooling

  • orb: The compiler will be written over LLVM platform.
  • orb-format: The code formatter.
  • orb-lsp: The language server protocol.
  • orb-prof: The profiler.
  • orb-doc: The documentation generator.
  • orb-test: The test runner.

Roadmap

It doesn't exist a roadmap yet, but the first step is to create a MVP (Minimum Viable Product) with the basic features, so:

  • PoC (Proof of Concept) of the language: implement the basic features as a clang plugin
  • v0.1: Implement the basic features in the compiler
  • v0.2: Implement the standard library
  • Until v1.0: Iterate over the language, add new features, remove others, etc.
  • v0.9: Feature freeze, only bug fixes
  • v1.0: Release, the language is stable

After the v1.0 the development will be separated in two types of releases: 'major' and 'minor'.

  • Major releases can introduce breaking changes
  • Minor changes only can be bugfixes or patches

With this approach the language don't need to have an 'LTS' version, major versions will be. In the far future the develop of an v2.0 may start, but it will feel like a new language, not a new version.

Notes

  • Due compatibility, Orbis will need at least AVX compatible CPUs
  • All things are subject to change
  • The official serialization formats will be: YAML for human readability, binary for performance
  • The build system will be the language itself
  • All configs will be in YAML format

Why?

Section to explain some questions. This section and the next are opinionated.

Why create a new language?

Because me, the author, have a specific need: a language that can be implemented in my own game engine. Also, I want to create something new and learn from the process. I have been coding in the last +10 years (I'm 23 at the time of writing this), and I want to create something that I can be proud of. Also, I saw a lot of languages that are good, but they are not perfect, so I want to create something that can be perfect for me and, maybe, for others.

Why the name 'Orbis'?

Initially it was only a codename but I like it.

Why no 'static' keyword?

The 'static' keyword have little to no sense in a no forced OOP language. It usually hides an architectural problem instead of resolving it. Also, 'static' in many languages is actually 'syntactic sugar' for other things.

Why meta-programming?

Meta-programming is a extremely powerful tool that enables the developer to write less code, making the iteration faster and the code more readable. Also, Orbis is a extremely modular language, so meta-programming is a must. The standard library will use a lot of meta-programming.

Why no 'try/catch'?

I think that the 'try/catch' system can be used in some cases to not handle errors, but instead hide them, like in global error handlers. Also, from a performance perspective, they provide a lot of overhead. The replacement with 'Result' and 'Option' types is more explicit and more efficient.

Why no 'null' (only for optionals)?

null is a special value in many languages but it have no sense from a memory perspective because it's value is not other than a pointer to the memory address 0. This causes that developers can use null in many cases where they should not. I think that a better approach is to use Option and Result types, they force the developer to handle the cases where the value is null or the operation fails.

Philosophy

About bugs

  • Compiler errors are better than runtime bugs
  • Rescues are better than panics
  • Runtime crashes are better than undefined behavior

About code

  • The best code is the no code
  • Readability over performance (without compromising efficiency when critical).
  • Trust the compiler, it's smarter than you

About languages

  • Programming languages must adapt to developers, not the other way around
  • Small and simple core, with a powerful standard library
  • Easy to learn, challenging to master

About performance

  • Computers are getting faster, but not forever
  • Compile time is an abundant resource; use it for optimization.
  • Resources of the end user are limited

About software development

  • 80% of the work takes 20% of the time
  • Edge cases need to be handled; they aren't optional
  • Reusable code is better than single-use code

License

Everything related to Orbis will be under the Apache License 2.0, everyone can use it without restrictions.