raga

Raga

// one raga. every rendition.

Raga is a small, explicit language built from one shape. A variable, a function, a type, a class, a loop and a call with a block all follow one pattern.

The language is described by examples written in Raga itself. Start with the templates, they explain one area each, in order.

[[attributes]]
[ const | let | Type ]  name  =  ( head [(args)] [|params|] [Type] [{ body }] )+

If you can read that line, you can read Raga.

01The one shape

Every declaration has the same parts, in the same order. Learn the parts once and the whole language reads the same way.

PartMeaning
[attributes]Facts about the declaration, written on the line above it. Optional.
const / let / TypeHow the name is bound. Exactly one of the three, never a type together with const or let. const cannot be rebound, let can, a type first picks the type: u8 level = 5.
nameWhat you call it.
=Binds the name to what follows.
headWhat is being made or done: a value, a type name, function, method, struct, enum, union, class, interface, trait, attribute, get, or a call.
(args)The values that go in: parameters of a function, arguments of a creation, the condition of an if.
|params|The names that the head hands to its block: for (items) |item| { }, catch |e| { }.
TypeThe type of the result.
{ body }The code, or the members.
( … )+The chain: the same parts again, with a head of its own, on the line of the closing }.

The same shape, in different clothes

const limit = 100                                  // a value
u8 level = 255                                     // a value, with its type chosen first
const area = function(f64 radius) f64 {            // a function
    return(3.14159 * radius * radius)
}
const Point = struct { f64 x; f64 y }              // a type
const Color = enum { const Red = 0; const Green = 1; const Blue = 2 }   // a type
const route = attribute(String path)               // an attribute

Without a name, it is a statement

A condition, a loop and a call with a block are the same line without the binding: head (args) |params| { body }, repeated as often as needed.

if (temperature > 30) {
    weather = "hot"
} elif (temperature > 20) {
    weather = "warm"
} else {
    weather = "cold"
}

try {
    check_age(200)
} catch |e| {                                      // the next clause starts on the line of the }
    print(e.message)
} finally {
    cleanup()
}

02Philosophy

Raga is small on purpose. Each idea follows from the shape, and nothing is done behind your back.

Everything is a binding

A function, a type and an attribute are values bound to a name, declared like any variable: const area = function(…), const Point = struct { }. There are no special declaration forms to learn.

One bracket, one meaning

( ) values, < > types, { } a body or the members, | | names handed to a block, [ ] attributes on the line above a declaration.

Nothing is hidden

No implicit conversion, no hidden return, no hidden this, no null, no overloading of plain functions. return is a call, return(value) or return(), always written. A name must be declared above its use.

The name tells the kind

A primitive (i32, f64, bool, lowercase) is a plain value that is copied. Everything else (String, List, Function, every struct and class, Capitalized) is a value too: assignment copies it, with everything inside. There is no clone. To share, point at it with addr(x). const protects the whole value.

Failure is part of the type

A call that can fail returns Optional<T> or Result<T, E> and is read with match. What should never happen is thrown with throw(Error(…)) and handled with try.

Memory is scoped, not counted

A value is freed when the block of its name ends, so there are no cycles and no pauses. A pointer, p.*, never keeps anything alive. alloc and free exist for the rare case that needs manual memory.

Facts go above the declaration

An attribute is written in [ ] on the line above. [page("/")] is data your code can read, [method(Ownership.Static)] changes what a method is. Modifiers are members of enums, not reserved words.

One file, one module

export { a, b } ends a file and import("./path") reads another. There is no main function: the file you run goes from the first line to the last.

Conventions

Types, enum members, variantsCapitalized
Variables, parameters, fields, functions, attributeslower_snake_case
Methods and fields of a class, interface or traitlowerCamelCase
LayoutA clause starts on the line of the } before it. match and switch hold one case per line.

03Language features

The ideas that follow from the shape, each shown in a few lines.

Types in angle brackets, values in parentheses

List<i32> and Map<String, i32> name a type. area(2.0), Point(x = 1.0, y = 2.0) and u8(300) take values. A cast is a type name called on a value. Collections have literals that read like their constructors: "text", {1, 2, 3} (Array), [1, 2, 3] (List), (1, 2.5) (Tuple).

Functions are values, and closures

A function can be declared anywhere, uses the names around it, and can return another function. What a closure uses moves to the heap and lives as long as the function does.

const make_adder = function(i32 step) Function<[i32], i32> {
    const add = function(i32 n) i32 { return(n + step) }
    return(add)
}
const add_two = make_adder(2)
const seven = add_two(5)

Trailing blocks

The last argument of a call can be written as a block after the parentheses. It is a function body for a callback, the values of an Array, or the name = value lines of a struct, and the type of the last parameter says which. When the block is the only argument, the parentheses are left out. A name without parentheses is the function itself, never a call: double is the function, double(5) calls it.

const apply = function(i32 value, Function<[i32], i32> transform) i32 {
    return(transform(value))                          // the last parameter is a callback
}
const double = function(i32 n) i32 { return(n * 2) }

const doubled = apply(4) |n| { return(n * 2) }        // the block fills the last parameter
const ten = double(5)                                // a call always has parentheses

const Column = struct {
    String name
    i32 size
}
const table = function(String name, Column column) { }
const tally = function(String title, Array<i32> numbers) i32 { return(numbers.len()) }

table("users") {                                     // the struct is the block...
    name = "id"
    size = 4
}
const total = tally("scores") { 4, 8, 15 }           // 3, the Array is the block

Chains of blocks, built with structs

Calls that return values are chained with a dot, user().select().limit(1). When the last argument of a call is written as a block (a callback, an Array or a struct), a dot after the closing brace would look strange, so the dot is left out and the next name follows the brace: user { } select { } limit (1).

For your own code a function takes one trailing block, for its last parameter, so a chain of blocks is built with structs: each step returns a struct that has only the methods which may come next. The return type of a step decides which clause can follow.

when(a > 3) {
    print("big")
} or_when(a > 1) {
    print("small")
} otherwise {
    print("none")
}

Classes, with the same shape inside

A field is a variable, a method is a binding whose head is method, and overloads and accessors are chains on the closing brace.

[class(Inheritance.Final)]
const Student = class {
    String name = ""
    const greet = method() String {
        return(this.name)
    } method(String greeting) String {             // an overload is a chain
        return(String.join(greeting, this.name))
    }
    const marks = get { return(this._marks) } set |value| { this._marks = value }
} extends(People) implements(ISpeak)

Failure is in the type

const half = function(i32 n) Result<i32, String> {
    if (n % 2 != 0) { return(Result.Err("odd")) }
    return(Result.Ok(n / 2))
}
match (half(10)) {
    case (Ok)  |ok|  { print(ok.value) }
    case (Err) |bad| { print(bad.reason) }
}

Attributes, in three patterns

You declare your own attributes with the head attribute. They build on each other: metadata that code can read, a hook that runs once while building, and a hook that runs on every call. The block names everything it uses between its | |, so nothing arrives under a hidden name.

const metadata = attribute(String name)                               // 1. metadata only

const routes = Map<String, BindingInfo>()
const route = attribute(String path) |target, path| {                  // 2. a build hook
    routes.set(path, target)
} call |target, args, next| {                                          // 3. a call hook
    print(String.join("calling ", target.name))
    return(next(args))
}

[route("users/")]
const users = function(i32 first, i32 count) String { return("list") }

Build-time evaluation and reflection

compile(expression) runs ordinary code while the program is built and puts the result in the program as a constant. reflect<T>() describes a type as data, and attributes_of reads the attributes on a declaration.

const square = function(i32 n) i32 { return(n * n) }
const sixty_four = compile(square(8))                // computed by the compiler

const info = reflect<Account>()
const field_count = info.fields.len()

Modules

const shapes = import("./geometry/shapes")           // a file
const check = import("test")                          // a library by name, resolved through raga.project and the libraries folder

export { area, perimeter }                           // the last statement of a file

What Raga does not have

  • No null
  • Nothing to throw but an Error
  • No implicit casts
  • No overloading of plain functions
  • No inheritance for structs
  • No multiple inheritance
  • No operator overloading, except with the [operator] attribute
  • No main function

04Templates

Start with hello.raga for the whole language on one page, then read the templates in order. Each one explains one area in full, and none uses a feature that a later one explains.

Open the templates on GitHub

Editor support

The VS Code extension gives syntax highlighting, completion, hover, go to definition, snippets and errors as you type, including the trailing block and the next clause of a chain. Install it from the Visual Studio Marketplace, or read the source in vs-code-extension.