OMG, another programming language? Whyyy?? Are you serious with this??

Crescent is for C-like imperative programming but with modernized syntax, types, and program organization.

Development Maturity Level: Pet Project (but getting there!)

Highlights

Improved Type System

  • Strict type checking including for intrisic value-type aliases (e.g. if typedefs of ints in C were new strictly checked type names)
  • Type inference
  • Polymorphism support via (automatically discriminated) union types (e.g. covariant return types)
  • Run-time type testing

Better Array Types

  • Strings and arrays have known, fixed lengths which may be queried with the built-in function len()
  • Strings and arrays are not "just pointers" and may be value compared using the standard equality operator, ==

Modules

  • Code may be freely associated with a module by name by placing a module declaration statement in the source file
  • Data and functions are freely associated; no classes
  • Modules and data structures have no visibility modifiers (no data hiding)

Examples

Basic Main Function

Here’s just a bit more than just printing “Hello, World!”

const pl_name str = "Crescent";
var verbose bool = false;

fn main(args str[]) i32 {
    println("Hello, World!");
    println("Programming language: ", pl_name);
    println("Number of arguments: ", len(args));

    foreach (arg in args) {
        println("Argument: ", arg);
        if (arg == "-v") {
            verbose = true;
        }
    }

    return 0;
}

This simple program demonstrates several language attributes:

  • The prototype for the main() function for any program (your program entry point). It takes an array of strings (str[]) which are the command line arguments.
  • Constant and variable global declarations
  • Type names for several intrinsic types like integers, booleans, and strings
  • Variadic function arguments. E.g. println()
  • String value comparison using ==
  • Simple iteration over arrays using foreach (element in array)

Data Structures

Data structures in Crescent are essentially C-like structs.

@packed
struct Packet {
    opcode u8;
    payload_length u16;
}

@packed is an annotation. All annotations start with @ and are free-form. There are a limited number of annotations handled by the compiler but @packed is one of them. It tells the compiler to use 1-byte alignment (no padding between fields or at struct boundaries).

Enumerated Types (Enums)

Enums are explicitly of a type.

enum Color unit {
    Black;
    VeryVeryDarkGray;
}

enum OpCode u8 {
    NOP = 0;
    ADD = 1;
    SUB = 2;
    OMG = 3;
    HCF = 4;
}

fn handle_op_code(op OpCode) unit {
    if (op == OpCode.NOP) { }
    else if (op == OpCode.HCF) { while (true) { } }
}

fn batman_to_webcolor(color Color) str {
    if (color == Color.VeryVeryDarkGray) { return "#121212"; }
    return "#000000";
}

Type Aliases

Crescent has strict type aliases (which sounds like an oxymoron so maybe this should be called something else). You can alias a type and use it to differentiate different properties or units of variables and function parameters. Primarily this is used to solve the “sea of ints” function parameter problem in C and C++ where anything that could be interpreted as an int is quietly converted to an int.

type PressurePSI f32;
type TemperatureDegC f32;
type SalinityGramsPerLiter f32;

fn speed_of_sound_in_water(pressure PressureMPa,
                           temperature TemperatureDegC,
                           salinity SalinityGramsPerLiter) MetersPerSecond {
    // Do all that math and return the value
}

//   ---  8<  ---

var pressure PressurePSI;
var temperature TemperatureDegC;
var salinity SalinityGramsPerLiter;

//   ---  8<  ---

// Later...

var transmission_speed = speed_of_sound_in_water(temperature, pressure, salinity);

// ^ The above fails to compile with a type mismatch error.

These type alias names are quite verbose, but just for example purposes.

Easy Option and Try types

Use unions to create variants.

struct Result {
    value any;
}

struct Error {
    code i32;
    message str;
}

union Try {
    Result;
    Error;
}

fn do_process(uri str) unit {
    var response = request_the_stuff(uri);
    if (response is Error) {
        println("Error: Couldn't get the stuff:", response.message);
    }

    handle_response(response.value);
}
struct Some {
    value any;
}


struct None {
}


union Option {
    Some;
    None;
}

fn process() unit {
    var maybe Option = returns_an_option();
    if (maybe is Some) {
        // use maybe.value
    }
}

* There is a little run-time magic that makes this possible (e.g. during assignment). But more explicit generics-like type parameters over structs and unions is another option under consideration.

Variadic Functions

Crescent leverages its run-time type information and safe arrays to support variadic functions without an unholy union of pre-processor and compiler glue. The special function parameter type ... collects the rest of the function arguments and passes them in as an any[]. You can then use the is operator to type-check the array elements.

fn println(args ...) unit {
    foreach (arg in args) {
        if (arg is str) {
            //  8< for brevity
        }
        else if (arg is bool) {
            //  8< for brevity
        }
    }
}

* Something like a “spread” unary operator that expands an array to an argument list is being considered. This is a natural inverse-like operation to the ... function parameter “collect” type. This gets immediately abused in most programming languages that have it (e.g. JavaScript and Python) but that’s not disqualifying by itself.

About The Language

Uniform Syntax

Defining types, functions, variables in your program follow a similar ordering convention: what it is, its name, its definition.

Once a type has been defined, its intrinsic construct-type (type of aggregate) no longer needs to be used in the syntax. E.g. if you define struct Thing {}, “Thing” is the type name and and it does not need to be prefixed with the construct type (struct) like in C (the compiler knows what it is and so should you).

Value literals match the enclosing symbol in their definition (e.g. array literals are enclosed in []). This looks better and makes type inference of arrays possible as it does not look like an anonymous structure literal.

struct StructName { }
enum EnumName { }
union UnionName unit { }

const pi f32 = 3.1415926535897932;
const first_four_primes u8[] = [1, 2, 3, 5];
var number_of_high_fives u32 = 27;

fn function_name(arg1 StructName) unit { }

(I have come to prefer snake case except for user-define type names being CamelCase but you do as you see fit.)

Conscientious Trade-Offs

Sins to be Avoided

More details

Pre-defined Types (Intrinsics)
unit
any
bool
u8
u16
i16
u32
i32
u64
i64
f32
f64
str

These types are pre-defined by Crescent. If you are familiar with C or Rust the names will be familiar, if not:

  • Numeric types have a prefix and the number of bits in the representation:
    • The u prefix indicates an unsigned integer
    • The i prefix indicates a signed integer
    • The f prefix indicates a floating point number (IEEE-754)
  • unit is a unit type (used like void in C). It has no size and contains no information.
  • any is a union that includes all defined types (both built-in and user-defined); it can be approximately thought of as void* in C except for two important differences: the corresponding type information is intact, it is a value and not a pointer.
  • bool is the built-in boolean type; "true" and "false" are built-in keywords.
  • str is the built-in string type.

Making a pointer to a type is done by adding an asterisk, e.g. type_name*

Making an array of a type is done by adding brackets enclosing the array length, e.g. type_name[10]

Language Constructs / Statements

fnDefine a function.
structDefine a data structure.
enumDefine an enumerated type.
unionDefine a union type.
constDeclare a constant (it must be initialized).
varDeclare a variable (it may be initialized).
typeDeclare a new type (from an existing type).
moduleDeclare the module the code in the source file is associated with.

Flow Control

foreach

Simple array iteration (arrays are fixed-length).

var greeting_tokens str[] = ["Hello", " ", "world!", "\n"];

fn say_hello() unit {

    foreach (token in greeting) {
        // token is a string value,
        // copied out of greeting_tokens
        print(token);
    }

    foreach (token* in greeting) {
        // token is a string pointer,
        // pointing into greeting_tokens
        print(token);
        // ^ Might look questionable except print(args ...)
        // checks for pointer-to-string (str*).
    }

}

for

Essentially the same as in C except that variable declarations in the initial each require a type.

var greeting_tokens str[] = ["Hello", " ", "world!", "\n"];

fn say_hello() unit {

    for (i u32 = 0, n u32 = len(greeting_tokens); i < n; i++) {
        print(greeting_tokens[i]);
    }

}

while

The same as in C. Loops as long as the boolean condition is met.

fn main(args str[]) i32 {
    var exit_requested = false;

    while (!exit_requested) {
        process_data();
        read_inputs();
        display_outputs();
        exit_requested = check_exit_requested();
    }
}

if, else

The same as in C.

enum Fruit u8 {
    Apple = 0;
    Orange = 1;
    Banana = 2;
}

@packed
struct Packet {
    fruit Fruit;
    sender u32;
}

fn handle_packet(packet Packet) unit {
    if (packet.fruit == Fruit.Apple) {
        // ...
    }
    else if (packet.fruit == Fruit.Orange) {
        // ...
    }
    else if (packet.fruit == Fruit.Banana) {
        // ...
    }
    else {
        // Unknown fruit!
    }
}
Modules

Modules create python-style namespaces, so the function fun in the io module may be called with io.fun() in other modules. Directory structure and filenames do not dictate the module namespaces (unlike in python). This affords module mix-in and extension without modifying the original source module which may be from a 3rd party.

The source code in every file is associated with a module. The module association may be declared with module module_name;. There are two specially handled modules: _lang, and _app

Modules do not have data-hiding features and cannot be "sealed". Crescent does however have annotations and an annotation may be used to declare globals and function names as "private" but this merely keeps the corresponding code from being included in generated user (API) documentation. It also serves as an indicator to those modifying or extending a module to tread carefully.

Polymorphism

Crescent has no classes or polymophic class inheritance type model. Some forms of type parameters (generics) are under consideration.

Compiler Status

There is a compiler in works that uses the LLVM backend. The first-pass compiler is named C-Prime (c-prime) which is mostly being used to develop and test language features. It is therefore not yet focused on the user interface (which is to say compiler warnings and errors). It is a “no mistakes allowed!” type compiler which may only end up feature-complete enough to compile a self-hosted compiler (crescent).