Bm Language

By Sergio Pedraza @sergiouph

Overview

Bm is a general purpose programming language designed to be simple to write, read and maintain. It can be considered a multi-paradigm language since it is focused to provide robust solutions by enforcing principles not limited to a specific paradigm.

Principles

The design of the Bm Language is driven by a set of principles explained in this section. The principles were not just picked from the best-practices, they were synthesized organically from the evolution of the design of the language.

Trackable Origin

Every symbol in the code is defined in some place which can be found using simple rules.

A member can't be used if it is not explicitly imported or defined, even the primitive data types are not “automatically imported” to the modules, this forces the developer to import and track what specifically is used in a suite. Despite this may sound tedious and against the readability principle, the developer may leverage the access system and create a source file exclusively for importing the most used members across the suite, this may significantly improve the developer experience since some members can be used in an "auto-imported" fashion and be trackable at the same time.

import Int from bm

let a Int = 0      // OK
let b String = ""  // ERROR! `String` is not defined
Assuming this is the only source file in a suite, the type String is not defined since it is not imported.

Since shadowing is disallowed, the references can't be ambiguous.

let enabled = true

class Test {
  let enabled = false  // ERROR: `enabled` is already defined
}
Since enabled was defined in an upper level, the class Test cannot define a member with the same name.

Fields must be initialized, there are no default values.

class User {
  var id Int  // ERROR! `id` needs to be initialized
}
The member id causes a compilation error because it requires an initial value.

Readability

The written code must be easy to read and the developer must write the minimum amount of code.

The type inference allows to omit the type in most cases. When the type is not specified, it is determined by the initial value. The inconvenient with omitting the type is that sometimes the inferred type can be too wide.

let count = 0;                           // type: Int
let name = "Mat";                        // type: String
let rect = Rectangle.new(0, 0, 10, 10);  // type: Rectangle
let a = null;                            // type: Any
The type of the fields were automatically determined by the initial value.

Parenthesis after an if, for, while, etc., and separators between instructions are not required.

function main(args Array<String>) {
  if args.length == 0 {
    println("Missing arguments")
  }
}
The if block doesn't require parenthesis for the condition.

Extensions allow to have large class definitions and even so have small files.

// file: user.bm
class User {
  let id Int
  constructor new(@id Int) {
    id = @id
  }
}

// file: user.delete.bm
function User.delete() { /* ... */ }

// file: main.bm
function main() {
  User.new(10).delete();
}
The function delete is a member of the User class despite they are defined in different files.

Immutability over mutability

The language helps to reduce the amount of mutable members by forcing the developer to indicate it explicitly.

Parameters in functions are constant by default, variable parameters are allowed only by declaring them with the var modifier. When the modifier is omitted it is assumed to be let.

function test(a Int, var b Int) {
  a += 1  // ERROR: `a` cannot be reassigned
  b += 1  // OK: it was declared with `var`
}
The a parameter can't be reassigned unlike b that was explicitly declared to hold a variable value.

Interfaces may help to control if a field is mutable or not. When a type is treated as some interface, var fields in the original implementation can fit in let fields (disallowing the assignment), in contrast, let fields wouldn't fit in var fields causing a compilation error.

class Human {
  var name = "Mathison"  // It can be reassigned
}

interface Dog {
  let name String        // Cannot be reassigned
}

function test() {
  let h = Human()
  let d Dog = h          // Now it is a Dog! Not a Human
  d.name = "Amy"         // ERROR: `name` cannot be reassigned 
}
The interface Dog disallowed the reassignment of the field name despite the original implementation allows to be reassigned.

When a constant field is defined in a class, the initialization can be delegated to the constructor. This allows to have constant fields with computed values instead of variable fields initialized with some value and then reassigned in the constructor with the computed value.

class Cat {
  let name String  // OK: initialization is delegated to the constructor
  constructor new(@name) {
    name = @name   // OK: assignment must happen only in this block
  }
}
The name field was initialized in the constructor.

Declaring constants is as simple as declaring variables since they require the same amount of characters: let for constants and var for variables. This matters — some languages require extra effort to declare constants.

Comparison between variable and constant declarations across different languages.
Language Variable example Constant example
Bm, Swift var c = 0 let c = 0
Rust let mut c = 0; let c = 0;
Go var c = 0 const c = 0
JavaScript, TypeScript let c = 0 const c = 0
C, Objective-C int c = 0; #define c 0
C++, C# int c = 0; const int c = 0;
Java int c = 0; final int c = 0;

Loose Coupling

Interfaces help to describe how a value should look like without specifying a real type and templates help to inject types as parameters that can be used to write generic code. Both features may help to reduce the dependencies used in a suite.

// file: entities.bm
module entities {
  share class User {
    let key String
    constructor new(@key String) {
      key = @key
    }
  }
}

// file: storage.bm
module storage {
  share interface Item<ID> {
    let key ID
  }
  share class Container<ID> {
    function save(item Item<ID>) { 
      // It only requires something with a key so it can be saved.
    }
  }
}

// file: main.bm
import Container from storage
import User from entities
function main() {
  let container = Container<String>()
  let user = User("3e3b3142")
  container.save(user)
}
Modules entities and storage don't share anything, however, they can be used together.

Extensions make possible to add features to existing classes and interfaces without changing the original definition. The extension member is recognized as a normal member of the type as long as it can be accessed from the module where it is going to be used.

// file: entities.bm
module entities {
  share class Artist {
    var firstName String
    var lastName String
  }
}

// file: main.bm
import Artist from entities

getter Artist.fullName String {
  return "${this.firstName} ${this.lastName}"
}

function main() {
  let a = Artist(firstName: "John", lastName: "Coltrane")
  assert a.fullName == "John Coltrane"
}
The fullName field is recognized despite it was defined in a different module.

Even though multiple inheritance is disallowed, a class or interface can be composed explicitly with any amount of other interfaces. This can be done when the type is being defined or after the definition using the compose feature.

import Array from bm

interface Item {
  operation gt(other Item) Bool
}

interface Sortable<T Item> {
  let length Int
  
  operation get(index Int) T

  operation set(index Int, value T)

  function bubble_sort() {
    for i = 0 while i < length step i++ {
      for j = 1 while j < (length-i) step j++ {
        if this[j-1] > this[j] {
          let aux = this[j-1]
          this[j-1] = this[j]
          this[j] = aux
        }
      }  
    }  
  }
}

compose Array<T> implements Sortable<T>

function main() {
  let items = [3, 1, 2]  // Produces an Array<Int>

  items.bubble_sort()

  assert items == [1, 2, 3]
}
The function bubble_sort is recognized since the class Array was composed with the Sortable interface.

Language

Bm can be considered as a part of the C-family languages, the code written in Bm is free-form, there is no pattern to follow regarding the organization of the source files.

Structure

Bm projects, also named Suites, can be organized using two concepts: Members and Containers. Some containers are also members and some members are flat.

The definition of the members and containers of a suite depends on the directory organization and the content of the files. All files and directories are used for defining the suite.

Containers

The containers are used for collecting members, all containers have a name, but not all containers can be referenced in the code.

The root container for all members and containers is the suite, which can be considered as a module that cannot be referenced. A directory containing at least the suite file is considered a valid suite definition.

Besides the suites, there are other containers defined by using the file system:

  • Source File (*.bm): Any kind of member can be defined in a source file. The filename is not relevant since they cannot be referenced. At compilation time, it doesn't matter the amount or the order of the source files in a directory, they can be considered as a single content.
  • Module (implicit): Any directory inside a suite define a module with the same name, naturally they only can contain files and more directories, however, the members defined in the source files are owned by the module and the directories are considered sub-modules.

There are other containers that can only be defined by writing Bm code:

  • Module (explicit): They are defined with the module keyword and can contain any kind of member. Module definitions are not unique, the members are merged when any other kind of module definitions are found.
  • Class: Can contain Fields, Functions, Getters, Setters, Operations and Constructors. The Fields of a class are the only members that cannot be added by using extensions.
  • Interface: Can contain the same members than a class, except fields. Members of an interface can be defined by using extensions as well.
  • Enum: Can only contain enumerated fields, however, Functions, Getters, Setters and Operations can be added only by using extensions.

Members

Members are owned by a container and have a name. The name of a member in a container must be unique, however, two or more members can be grouped using the same name when they are overloaded or when they are a getter and setter pair.

Classes, interfaces, enums and modules are members that are containers as well. The modules are the only members that can be nested indefinitely. Classes, interfaces and enums are owned exclusively by modules.

Members listed below can be overloaded, in other words, multiple definitions can share the same name as long as the overloading rules are followed:

  • Functions: Can be defined in modules and classes. Naturally, when defined in classes, an instance is required so they can be invoked.
  • Constructors: Can be defined directly inside a class but in a module, they can be defined only through extensions.
  • Operations: Have the same definition rules than the constructors but the name is restricted to the allowed operations.

Fields are the most simple members since they can't receive parameters or have a body. They can be defined in modules and classes, behaving just like the functions.

Getters and Setters can share the same name since, together, they can be treated like a field. They can be defined in modules and classes as well.

Suites

Suite is a Bm concept for representing projects, solutions, programs or packages. This concept is important since Bm is not designed for scripting, said in other words, the design doesn't promote having stand-alone files:

Suites can be compiled to a distributable file format containing the necessary information to be imported in other suites or executed in any platform. Only suites containing an Entry Point can be executed, however, all suites can be imported in other suites.

Suite File

A suite file is a JSON file named bm.json, it describes the suite represented by the folder where the file is. This folder is considered the Suite Module.

Required fields in a suite file.
Name Type Description Default value
id String It is an UUID identifying the suite, it is analogous to a fingerprint. This value should never be changed since it is used to validate the correctness of a suite reference. Random UUID.
name String The name of the Suite Module. This name is used by other suites for importing the members from this suite. The name of the suite folder.
version String

Is a value composed by three numbers in the following format: major.minor.patch. The numbers between a versions should be incremented by following these criteria:

  • Major: changes breaking backward-compatibility.
  • Minor: changes introducing new features.
  • Patch: changes solving bugs.
0.0.0

Despite previous fields are required, they are auto-filled with the default values when they are missing.

Entry Point

A suite can be executed only if it contains an entry point which normally is a function in the suite module named main, however, it can be changed in the suite file.

// main.bm
function main() {
  // program logic
}
Example of an entry point.

Reference System

This system defines the rules for referencing members in the code based on the relationship between containers and members. Referencing a member doesn't guarantees the access to it, the access to the members is managed by the Access System.

Since all members have a name, containers can be viewed as a list of names and each name could contain more names since some members are containers too. This structure of names is delimited by following rules:

Naming Rules

Any name can be given to a member, but the way to represent the names in the Bm Language obbey two formats:

  • One letter followed by more letters or digits.
  • Delimited string using grave accent (`).

The characters that are considered letters are from a to z (lowercase), from A to Z (uppercase), _ (underscore), $ (dollar sign) and @ (at symbol). The delimited string follows the Character Escaping rules.

TODO Add information for operators.

Member Path

Since the name of a member is unique, members can be referenced unambiguously using the member path which can be generated by joining the path of the container and the name of the member with a point (.), containers without name are ignored.

                                        // Member Path:
module math {                           // "math"
  let PI = 3.141592;                    // "math.PI"
  function max(a int, b int) int {      // "math.max"
    return a > b ? a : b;
  }
  module geo {                          // "math.geo"
    function tan(value double) double { // "math.geo.tan"
      // fancy maths...
    }
  }
}
Member Path example

Shadowing Disallowance

Naming a member with the same name than a member that can be referenced is not allowed because it generates ambiguity.

let x = 0;

class Point {
  var x = 0; // ERROR: The member "x" is already defined in the parent module.
  var y = 0;

  function moveTo(@x, y) { // ERROR: The member "y" is already defined in the class "Point".
    let Point = null; // ERROR: The member "Point" is already defined in the parent module.
  }
}

Access System

This system refers to how the members are accessed through the modules. This is controlled by using a set of keywords called access modifiers which define the rules to access to some member from a certain module.

How access modifiers work
Who can access to the member? protect (default) share export
Same declaring container
Same module in the same suite
Any module in the same suite
Any module in any suite

Protect Modifier

The protect modifier is the most secure modifier, the access to members declared with this modifier is limited only to the container where the member was declared, they can't be imported or accessed by other source files.

class Foo {
  protect let BAR = 1;
}

protect let BAZ = Foo().BAR + 1; // ERROR: `BAR` is not accessible
file1.bm
protect let QUX = BAZ + 1; // ERROR: `BAZ` is not accessible

module abc {
  let XYZ = QUX + 1; // OK! `QUX` is accessible
}
file2.bm

Default Modifier

The default modifier is the most handy modifier, to declare a member with this kind of access the member shouldn't have an access modifier at all.

This modifier makes that the member can be accessed only by the members defined in the same module or submodules of the same suite, other modules can't import or referencing the member. It keeps being secure, in the sense that it encapsulates the member in the declaring module.

let FOO = 1;
file1.bm
module bar {
  let BAZ = FOO + 1; // OK! `FOO` is accessible
}
let QUX = bar.BAZ + 1; // ERROR: `BAZ` is not accessible
file2.bm

Share Modifier

The share modifier has two behaviours depending on the member:

  • Class members: allows the member to be available in any subclass no matter the suite.
  • Other members: allows the importing and referencing of the member across the suite.
module debug {
  share function log(message String) { /* ... */ }
}
file1.bm
module foo {
  import debug.log;

  function bar() {
    log("bar..."); // OK! `log` is accessible
  }
}
file2.bm

Export Modifier

The export modifier should only be used when the member needs to be exported outside the suite. Members declared in the same module or submodules have access to the declared member without importing it and the member can be imported in any module of any suite.

export function foo() { /* ... */ }

share function bar() { /* ... */ }
Suite 1 (foobar)
import foobar.foo; // OK! `foo` is exported
import foobar.bar; // ERROR! `bar` is not exported
Suite 2 (barqux)

Modules

A module is a container for Fields, Functions, Classes, Properties, Enums, Interfaces, Extensions and more Modules. The access to the members is determined by its module access and they are always available during the runtime.

Global Module

The global module cannot be referenced inside of the suite, however, when using another suite as dependency, the global module of the other suite can be referenced by the name of the suite.

// the member is declared in the global module of the "foobar" suite
export function doSomething() { /* ... */ }
Library Suite "foobar"
// the global module of the library "foobar" is now "foobar"
import foobar.doSomething;

export function main() {
  doSomething();
}
Application Suite (depends on "foobar")

Explicit Modules

The explicit modules are created by using the keyword module.

module math {
  // ...
}

Implicit Modules

Modules are defined at file system level by using directories. The root directory is considered the global module and all other modules are relative to it. The source files contained in a module define the corresponding members.

/some/path/
|- awesome-app/              : Global module (root directory)
|  |- math/                  : Module 'math'
|  |  |- physics/            : Module 'math.physics'
|  |  |  |- mechanics.bm
|  |  |  |- fluids.bm
|  |  \- geometry/           : Module 'math.geometry'
|  |     |- topology.bm
|  |     |- trigonometry.bm
|  |- data/                  : Module 'data'
|  |  \- local-storage.bm
|  \- main.bm

Imports

Shared and exported members can be imported into another module. The imported members become also members of the module which is importing them. To avoid adding complexity, the imported members cannot be renamed. The module access rules applies as well as any other definition, this means that the import statements can be declared with a module access modifier:

Import Syntax

Multiple members can be imported using the same sentence, if the module is indicated all imported members must be of the same module.

module A {
  share module B {
    share let X = 1;
    share let Y = 2;
    share let Z = 3;
  }
  share module C {
    share let I = 1;
    share let J = 2;
    share let K = 3;
  }
}

module example1 {
  import A.B.X, A.B.Y, A.B.Z,
         A.C.I, A.C.J, A.C.K;
}

module example2 {
  import B.X, B.Y, B.Z
         C.I, C.J, C.K from A;
}

module example3 {
  import X, Y, Z from A.B;
  import I, J, L from A.C;
}

module example4 {
  import A.B, A.C;
}

Importing Modules

When a module is imported, all members of the module becomes members of the importing module.

Extensions

Extensions allow to add members to existing types without modifying the existing definition or creating a new type. The way to define extensions is by prefixing the member name with the type reference followed by two colons (::), only members with logic can be used as extensions:

The body of the extension member, just like Class Members, has access to this representing the instance and all other instance members will be available.

class User {
  var name String = null;
}

function User::delete() {
  Console.log("Deleting user ${name}"); // OK! `name` is available
}

When an extension is defined in the same module and suite than the extended type, the extension member will be treated as a member of the type, so when the type is imported it will have the extension members available.

module lib {
  share class Collection { /* ... */ }

  share function Collection::clean() { /* ... */ }
}
module app {
  import Collection from lib;

  function test() {
    Collection().clean(); // OK! `clean` is available
  }
}

If the extension member is declared in a different module than the extended type, it needs to be imported explicitly so the type can have the extension member available.

module lib {
  share class Collection { /* ... */ }

  module actions {
    share function Collection::clean() { /* ... */ }
  }
}
module app {
  import Collection from lib;
  import User::clean from actions.lib; // this line is required for the extension
  function test() {
    Collection().clean(); // OK! `clean` is available
    clean(); // ERROR: `clean` is not defined
  }
}

Templates

Generic programming is implemented by adding a template signature to Classes, Interfaces, Lambdas, Functions and Operators. The template signature can define one or more Template Types associated with another type, when the associated type is not specified it is assumed to be Any. Template Types are considered valid types in the inner implementation.

Template Members can only be used if the Template Arguments are clear, it can happen in two ways:

interface Comparable<T> {
  operator `==` (value! T) Bool;
}

interface Entity<ID> {
  var id Comparable<ID>;
}

class User {
  var id Int;
}

class Container<ID> {
  function store(entity Entity<ID>) { /* ... */ }
}

lambda Storer<T>(item T);

function createStorer<ID>() Storer<ID> {
  let container = Container<ID>();

  return container.store;
}

function main() {
  let store Storer<Int>  // Explicit Template Arguments
    = createStorer();    // Inferred Template Arguments
  let user = User();

  store(user);
}

Overloading

Members that accepts parameters can be overloaded, this means that more than one definition can be provided with the same name as long as the parameters at the same position are not compatibles with each other. There is no restriction on the number of parameters or if they are optional.

function print(value Any) { /* ... */ }
function print(value String) { /* ... */ }
// ERROR! `String` is compatible with `Any`.
Invalid Overloads
function max(a Int, b, Int) Int { /* ... */ }
function max(a Single, b Single) Single { /* ... */ }
// OK! `Int` and `Single` are not compatibles
Valid Overloads

Extended Functions

Overriding inherited functions in classes is disallowed, however the way of adding behaviour is by using before and after.

Since in Bm is not possible to override an inherited function, an alternative might be to add aspects to the class so it can be possible to execute some block of code before or after the function is called.

Aspects should be directly added only to inherited functions to avoid obscuring the logic of normal functions.

The block of code to be executed before or after the function should not have return type, the parameters for before blocks, should be exactly the same than the target function and the parameters for after blocks, should be the same than the target function plus the return type only when it applies. In this way, aspects will be able to mutate the input and output of a function, but won't be able to change the references.

Only one aspect per before and after should be allowed in one class. The aspects added to functions which the superclasses already added aspects will be chained.

class Shape {
  var x = 0;
  var y = 0;

  function moveTo(@x Int, @y Int) {
    x = @x;
    y = @y;
  }
}

class Element {
  function redraw() { /* ... */ }
}

class Oval inherits Element, Shape {

  after moveTo(@x Int, @y Int) {
    redraw();
  }

  var width = 0;
  var height = 0;

  function draw(g Graphics) {
    g.drawOval(x, y, width, height);
  }

}

Initialization

All fields must be initialized explicitly by the developer, there is no concept of implicit default value like 0, false or null. A field can be only initialized by using one of the three ways described below.

Inline Initialization

This is the simpler way, the value is specified immediately after the declaration.

var count = 0;
let user = User.new("Mat");

Delegated Initialization

The initialization of the fields in a class can be delegated to the constructors.

The initial value for the field can be omited in the field's declaration, but the class must declare one or more constructors and each constructor must initialize the field.

class Entity {
  let id String; // initialization delegated

  constructor new(@id String) {
    id = @id; // initial value
  }
}

See Constructors for more details.

Function Parameter Initialization

Despite a function parameter can be considered as a field, the initialization is optional since the initial value is taken from the function invocation.

However, initial values in function parameters are allowed but it indicates an Optional Parameter.

function test(a String, b String = "bar") {
  // do something
}

function main() {
  // the initial values will be:
  // a = "foo" and b = "bar"
  test("foo");
}

See Function Parameters for more details.

Type System

Bm is a language statically typed, all values are resolved to a type at compiling time. The usage of interfaces and templates makes possible to write code for abstract types, however, at compiling time the type abstractions are resolved to well-defined types.

Following list shows the types that can be declared:

References

In Bm all values are passed by reference, this means that if a value is passed to a function, the function is able to modify the received value without creating a copy.

There is no way to indicate that the value should be passed by value instead of reference (creating a copy), however, the compiler, depending on the platform, can decide how to pass the value based on the mutability of the value.

Null Values

TODO: Values can be null only if they are optional.

In Bm all references can be null by default no matter the type of the field, the only way to avoid null values in a field is declaring it with the non-null modifier (!) after the name. When a field has a null value, it means that the field doesn't have a valid value. If a field is declared as non-null, it is guaranteed to never hold a null value. Null value assignments to non-null fields will throw an error at runtime, compilation errors only happen if the assignment can be detected by the compiler.

Type Inference

All values' types can be inferred at compiling time, but in some cases the type has to be declared explicitly to avoid generalities or specificity. The most generic value that can be assumed is Any and the most specific will depend on the initial value.

// if the type is not specified it would be Any
var name String = null;

// if the type is not specified it would be bool
var value Any = true;

Type Casting

Type casting makes possible to treat a value as if it was of other type without creating another value. Syntax:

(TargetType)sourceValue

When the type of the source value and the target type are not compatible an error is generated.

TODO: Complete adding examples and indicating if it is generated at compiling time, runtime or both.

Natives

TODO Explain that can be implemented by the compiler, list expected natives, explain how to pass values from bm.json to runtime.

Built-in Types

Bm is not oriented to a specific platform or hardware, however, in order to operate it defines a set of native classes which are assumed to exist and can be used to create more complex types.

Built-in Classes
Type Size DescriptionRange
Byte 8 bits Smallest signed integer. −128 to +127
UByte 8 bits Smallest unsigned integer. 0 to 255
Short 16 bits Medium-sized signed integer. −32,768 to +32,767
UShort 16 bits Medium-sized unsigned integer. 0 to 65,535
Int 32 bits Default signed integer.−2,147,483,648 to +2,147,483,647
UInt 32 bits Default unsigned integer.0 to 4,294,967,295
Long 64 bits Biggest signed integer.−9,223,372,036,854,775,808 to +9,223,372,036,854,775,807
ULong 64 bits Biggest unsigned integer.0 to 18,446,744,073,709,551,615
Single 32 bits Single-precision floating-point number
Double 64 bits Double-precision floating-point number
Bool 8 bits true or false
Char 16 bits Any unicode character.
String Depends on the instance. Stores a sequence of Chars.
Array<T> Depends on the instance. Stores a sequence of values of the same type.

As well as specific classes are expected to exist, some interfaces are necessary to provide complete and decoupled support of some features of the language.

Built-in Interfaces
Type Description
Any Represents any value.
Tuple<…> Stores a sequence of values of specific types per item.
Error Describes an error at runtime.

Literals

Numbers:

function main() {
  let way1 = (Byte)127;
  let way2 Byte = 127;
  let way3 = (n Byte) => {};

  way3(127);
}

Classes

Classes are custom types that can be instantiated into values using constructors. All other members declared in the class than the constructors can only be accessed through an instance of the class. Only some Built-in Classes can be created using literals, without invoking a constructor.

class User {
  let id Int;
  var name String = null;
  var email String = null;

  constructor new(@id) { id = @id; }

  function delete() { /* ... */ }

  getter displayName String { return "${name} <${email}>"; }

  operator == (other! User) Bool { return this.id == other.id; }
}
Class Implementation
class User {
  let id Int;
  var name String = null;
  var email String = null;

  constructor new(@id) { id = @id; }
}

function User_delete(user User) { /* ... */ }

function User_get_displayName(user User) { return "${user.name} <${user.email}>"; }

function User_equals(User left, User right) {
  if left == null { return right == null; }
  else if right == null { return false; }
  else { return left.id == right.id; }
}
Equivalent Implementation

Class inheritance

TODO remove multi-inheritance and allow interfaces to have defined functions (but not initialized fields)

Classes can inherit from one or more classes only following these rules:

NOTE: See Type System for more details.

// represents an identifiable thing
class Entity {
  export let id String;

  share constructor new(@id String) {
    id = @id;
  }
}

// represents a record with control fields
class Record {
  export let createdAt Date;

  share var $updatedAt Date = null;

  share constructor new() {
    createdAt = Date.now();
  }

  export getter updatedAt Date {
    return $updatedAt;
  }
}

// represents the user entity record
class User inherits Entity, Record {

  protect var $name String = null;

  // must be chained to the superclasses' constructors
  export constructor new(@id String)
    : Entity.new(@id), Record.new() {}

  export getter name { return $name; }

  // non-protected superclasses' members can be accessed
  export setter name(@name String) {
    $name = @name;
    $updatedAt = Date.now();
  }
}

function test() {
  let user = User.new("0001");
  console.log(user.id);          // prints 0001
  console.log(user.name);        // prints null
  console.log(user.updatedAt);   // prints null
  console.log(user.createdAt);   // prints a Date

  user.name = "Mat";

  console.log(user.name);        // prints Mat
  console.log(user.updatedAt);   // prints a Date
}

Class Extension

TODO: Keyword extends. Only one superclass allowed. Inherits everything even the constructors.

Interface implementation

Despite it is not necessary, a class can explicitly implement one or more interfaces. The compiler will verify that the class comply with the definition of the interfaces. The interfaces specified will obbey the Interface Inheritance rules. If a function specified by an interface is not implemented, the class will be trated as Incomplete Class.

interface Drawable {
  function draw(g Graphics);
}

interface Shape {
  getter origin Point;
  getter size Size;
}

class Rectangle implements Drawable, Shape {
  export var origin Point = null;
  export var size Size = null;

  export function draw(g Graphics) {
    g.drawRect(origin.x, origin.y, size.width, size.height);
  }

  export constructor new() {}
}

class Oval {
  export var origin Point = null;
  export var size Size = null;

  export function draw(g Graphics) {
    g.drawOval(origin.x, origin.y, size.width, size.height);
  }

  export constructor new() {}
}

function test() {
  let g = Graphics.new();
  let shapes = ArrayList<Shape>.new();

  shapes.add(Rectangle.new());

  // next line is correct despite it was not explicit in the class definition
  shapes.add(Oval.new());

  for shapes : shape {
    shape.draw(g);
  }
}

Incomplete Class

Since classes can inherit from other classes, the complete definition can be delegated to subclasses. Rules:

Interfaces

Interfaces are descriptors of types. Any value can be treated as an interface without explicit casting as long as the type of the value matches with the definition of the interface.

interface Name inherits SuperInterface1, SuperInterface2 {
  function doSomething(param1 ParamType) ReturnType;
  getter someField Type;
  setter someField(value Type);
}
Interface syntax example

Interfaces can only contain Functions, Properties and Operators.

Interfaces can inherit from other interfaces, however if two or more superinterfaces have a member with the same name they must exactly match, otherwise it will cause an inheritance collision.

Interfaces are resolved to classes at compiling time depending on the usage. The compiler will create an specific implementation per each usage.

interface Sortable {
  operator < (value Sortable);
}

function sort(items Array<Sortable>) { /* ... */ }

function main() {
  let ids = [10, 54, 32];      // Array<Int>
  let names = ["D", "B", "X"]; // Array<String>

  // in order compile, `Int` and `String`
  // must implement the `<` operator.
  sort(ids);
  sort(names);
}
Implementation with Interfaces
function sort_Array_Int(items Array<Int>) { /* ... */ }
function sort_Array_String(items Array<String>) { /* ... */ }

function main() {
  let ids = [10, 54, 32];
  let names = ["D", "B", "X"];

  sort_Array_Int(ids);
  sort_Array_String(names);
}
Equivalent Implementation

Lambdas

Lambdas are types that represents functions. When a field is declared with a lambda type, the field can be treated as a function. Any member that contain logic can be converted to a lambda value as long as it matches with the lambda type. Lambdas can also be created inline.

Lambda Creation

There are more than one way to create a lambda:

Inline Lambdas

Inline lambdas are created inside instruction blocks, a closure is generated when fields are captured in the lambda:

lambda Action();
lambda Logger(message String);
lambda Operation(a int, b int) int;

function main() {
  // receives one arguments doesn't return a result
  let log Logger = (msg) -> { /* ... */ };

  // receives two arguments a returns a result
  let sum Operation = (a, b) -> a + b;
  let div Operation = (num, den) -> {
    if den == 0 { throw Error.new(); }

    return a / b;
  };

  // no arguments, no result
  let test Action = () -> {
    log(sum(1, 2)); // 1 + 2 = 3
    log(div(3, 3)); // 3 / 3 = 1
  };

  test();
}

Function Lambdas

Functions can be converted to lambdas as long as they match with the type.

lambda Writer(message String);

function print(message String) { /* ... */ }

class Console {
  function log(message String) { /* ... */ }
}

function main() {
  let write1 Writer = print;
  let write2 Writer = Console().log;

  write1("test 1!");
  write2("test 2!");
}

Property Lambdas

Since properties contain logic they can be converted to lambdas as well.

var name = null;

getter hasName Bool {
  return name != null;
}

setter hasName(value Bool) {
  if value and name == null { name = "Default"; }
  else not value and name != null { name = null; }
}

lambda Getter() Bool;
lambda Setter(value Bool);

function test() {
  let get Getter = hasName;
  let set Setter = hasName;

  console.log(name);  // null
  console.log(get()); // false
  set(true);
  console.log(get()); // true
  console.log(name);  // "Default"
}

Field Lambdas

Since fields can be converted to properties, fields can be converted to lambdas in the same way than properties.

var version = 1;

lambda Getter() Int;
lambda Setter(value Int);

function main() {
  let getVersion Getter = version;
  let setVersion Setter = version;

  console.log(getVersion()); // 1
  setVersion(2);
  console.log(getVersion()); // 2
}

Operator Lambdas

Since operators contains logic, they can be converted to lambdas.

class Bag {
  operator `+` (bag! Bag) Bag {
    let merged = Bag();
    // some magic to merge both bags here
    return merged;
  }
}

lambda Merger(bag! Bag) Bag;

function main() {
  let bag = Bag();
  let merge Merger = bag.`+`;

  // both lines are equivalent:
  let r1 = merge(Bag());
  let r2 = bag + Bag();
}

Closures

Inline Lambdas can capture constant fields creating a closure.

lambda Logger(message String);

function createLogger(prefix String) Logger<String> {
  return (message) -> {
    // Notice `prefix` is captured in this lambda.
    console.log("${prefix} - ${message}");
  };
}

function test() {
  let warn = createLogger("[W]");
  let info = createLogger("[I]");

  warn("Danger! Danger!");           // [W] - Danger! Danger!
  info("Don't talk to strangers.");  // [I] - Don't talk to strangers.
}

Enums

Enums are a custom type which can only have a limited set of existing values.

Enums cannot have superclass. Enums cannot be initialized.

enum Alignment {
  LEFT,
  RIGHT,
  CENTER,
  START,
  END,
}

function test() {
  var a Alignment = null;

  a = Alignment.LEFT;    // OK
  a = Alignment("LEFT"); // OK
  a = Alignment(0);      // OK
}

Fields

Fields are variables or constants that have a value stored in the memory. All fields have a type which cannot be changed after the declaration and can be either explicitly declared or inferred through the initialization.

Fields can be only owned by following members and the lifespan will depend on the owner:

module math {
  let PI = 3.1415;
}

class Point {
  var x = 0;
  var y = 0;
}

function swap(list List<Any>, index1 Int, index2 Int) {
  let aux = list[index1];
  list[index1] = list[index2];
  list[index2] = aux;
}

Functions

Functions are invocable blocks of code which can receive arguments and optionally produce a result.

Only when the function has a return type specifed, the function must use the return statement to complete the execution with a result.

Functions can be only owned by following members and the way to invoke them will depend on the owner:

function abs(n Number) Number {
  return n < 0 ? -n : n;
}

class Rectangle {
  var width = 0.0;
  var height = 0.0;

  constructor new(@width int, @height int) {
    width = @width;
    height = @height;
  }

  function computeArea() float {
    return width * height;
  }
}

function main() {
  // module functions can be invoked directly
  console.log(abs(-1));             // output: 1

  // class functions need an instance to be invoked
  let rect = Rectangle.new(10, 20);
  console.log(rect.computeArea());  // output: 200
}

Function Parameters

Functions can receive any number of parameters which are considered as fields available only during the function execution.

In order to improve readability, the keywords var and let are optional when declaring parameters and it is defaulted to let.

Since parameters are passed by reference, reassignments only affects the reference in the function's body.

function test(name String, var index int!) {
  name = "Mat";      // ERROR: The field can't be reasigned
  index = index + 1; // OK! the field is explicitly `var`
}

Optional parameters

Optional parameters are allowed by specifying the initial value when declaring them. Any parameter can be optional no matter the order, however, when invoking the function sending a list of arguments, optional parameters can't be skipped. The only way to skip an optional parameter is by invoking the function using Named Arguments.

It doesn't matter if the optional parameter is let or var, the initial value specified in the parameter declaration is only used when there is no value specified in the function invocation.

function write(data List<byte>, offset int! = 0, length int = null) {
  // ...
}

function test() {
  let data = [1, 2, 3];     // Optional arguments received:
  write(data);              //  offset = 0, length = null
  write(data, 1);           //  offset = 1, length = null
  write(data, 1, 2);        //  offset = 1, length = 2
  write(data, length: 2);   //  offset = 0, length = 2
}

Properties

Unlike normal fields, properties aren't allocated in memory, every time they are retrieved or assigned, the getter or setter logic is executed respectively.

Despite properties are declared separately by using getters and setters, they must comply with following rules:

When using interfaces, fields can be treated as properties transparently.

class DefaultApplication {
  export let version String = "1.0";

  protect $baseTitle String = "Default Application";

  export getter title() String {
    return $baseTitle + " v${version}";
  }

  export setter title(@title String) {
    $baseTitle = @title;
  }
}

class CustomApplication {
  export var title String = "Custom Application";

  protect let $majorVersion int;
  protect let $minorVersion int;

  export constructor new(@maj int, @min int) {
    $majorVersion = @maj;
    $minorVersion = @min;
  }

  export getter version() String {
    return $majorVersion + "." + $minorVersion;
  }
}

interface Application {
  let version String;
  var title String;
}

function test() {
  var app Application = null;

  app = DefaultApplication();

  console.log(app.title);   // "Default Application v1.0"
  console.log(app.version); // "1.0"
  app.title = "MyApp";
  console.log(app.title);   // "MyApp v1.0"

  app = CustomApplication.new(2, 3);

  console.log(app.title);   // "Custom Application"
  console.log(app.version); // "2.3"
  app.title = "MyApp";
  console.log(app.title);   // "MyApp"
}

Constructors

TODO: Default constructors are available only when no other constructor is defined.

Constructors are the only way to create an instance of a class, every class must have at least one constructor in order to be instantiable. Constructors can only be invoked through the class name, they don't need an instance to be invoked.

When a class is inheriting from another class with already defined constructors, the constructor must be chained.

A class can be designed to be only extended and prevent it to have direct instances, this can be only reached through protected constructors. Protected constructors can only be invoked in subclasses.

Constructors are not inherited so a constructor in a subclass can have the same name of a superclass constructor.

class Entity {
  export let id String;

  protect constructor new(@id String) {
    id = @id;
  }
}

class User inherits Entity {
  export let name;

  // `new` constructor can be declared since they are not inherited
  export constructor new(@id String, @name String)
    : Entity.new(@id) { // super-constructor can be accessed only here
    name = @name;
  }
}

function test() {
  let entity = Entity.new("0001");         // constructor is not accessible here
  let user = User.new("0001", "Mathison"); // accessible constructor
}

Operators

TODO: Change to operation?

Operators are special functions which can be overloaded.

Operator Symbols

Symbol Name Usage Description
== Equality this == other Returns a bool! indicating if this is equal to the other value.
> Greater than this > other Returns a bool! indicating if this is greater than the other value.
< Less than this < other Returns a bool! indicating if this is less than the other value.
>= Greater than or equal to this >= other Returns a bool! indicating if this is greater than or equal to the other value.
<= Less than or equal to this <= other Returns a bool! indicating if this is less than or equal to the other value.
[] Array access result = this[id] Returns whatever is in the id position of this.
[]= Array assignment this[id] = value Assigns the value to the id position of this.
class Vector {
  export var x = 0.0;
  export var y = 0.0;
  export var magnitude = 0.0;
  export var direction = 0.0;

  export constructor new(@x float, @y float, @m float, @d float) {
    x = @x;
    y = @y;
    magnitude = @m;
    direction = @d;
  }

  export operator + (other Vector) Vector {
    let result = Vector.new();

    // do fancy math here

    return result;
  }
}

function test() {
  let vec1 = Vector.new(0, 0, 10, 0);
  let vec2 = Vector.new(0, 0, 10, -180);
  let sum = vec1 + vec2;

  Console.log(sum); // { x: 0, y: 0, magnitude: 0, direction: 0 }
}

Instructions

TODO

Conditionals

If-Else

if condition1 {

}
else if condition2 {

}
else {

}

Switch

switch value {
  case A {

  }
  case B or C {

  }
  else {

  }
}

Loops

Infinite (loop)

loop {
  // do things

  if condition {
    break;
  }
}

Conditional (while)

let stack = Stack<int>.new([10, 20, 30]);

while stack.length > 0 {
  let item = stack.pop();
  console.log("item = ${item}");
}

// Output:
// item = 30
// item = 20
// item = 10

Incremental (for - while - step)

for i = 0 while i < count step i++ {
  console.log("i = ${count}");
}

// Output:
// i = 0
// i = 1
// ...
// i = 9

Iterative (for - in - else)

let list = [10, 20, 30];

for item in list {
  console.log("item = ${item}");
}
else {
  console.log("No items found");
}

// Output:
// item = 10
// item = 20
// item = 30

Error Handling

function main() {
  for i = 0 while i < 100 step 1 {
    if i == 23 {
      raise RuntimeError.new("I don't like 23");
    }

    rescue e RuntimeError {
      // I don't care
    }
  }
}

Expressions

TODO

Literals

TODO

Auto-Closing

using writer = path.createWriter();
writer.write([10, 20, 30]);

Named Arguments

find(text: "mat", fromIndex: 10);