OMG, another programming language? Whyyy?? Are you serious with this??
- Do you unapologetically enjoy low-level systems programming in C, are actually quite productive with it, but just wish it had a few modern "creature comforts" and safety checks?
- Do you like the Rust and Ada brochures, but when you get down to writing programs in them, feel an unwarranted level of friction? Do you find they inhibit exploratory or creative programming?
- Do you find the modern model of package management that is a growing tower of fractal abstractions that try to reach up to heaven to be problematic? Want to easily create, use, modify, and extend modules (packages)?
Crescent is for C-like imperative programming but with modernized syntax, types, and program
organization. Its approach is to fold in the things you would already be doing to make C-code
safe and sane (e.g. initializing with memset, bounds-checking, etc.) but not get
in your way when it comes to low-level tasks such as embedded systems programming
(bare-metal firmware on microcontrollers).
Development Maturity Level: Pet Project (but it's getting there!)
Highlights
Improved Types / 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
- Run-time type testing using an
isoperator - Optional initial values for
structfields - Variables are always initialized before use, either by the programmer or zero'd by the compiler
Improved Functions
- Call functions with ordered arguments or with named arguments (but not mixed)
- Variadic functions (natively)
- Define function-local inlined named functions (i.e. scoped native macros).
Better Array Types
- Strings and arrays have known, fixed lengths which may be queried by-field
with
.length foreacharray iteration loop syntax- Strings and arrays are not "just pointers" and may be value compared using the standard
equality operator,
== - Separate allocator and allocation segment for strings.
Work Deferral
- Use
deferstatements to defer function calls until the current scope closes. A clear win in languages that support this for e.g. always relinquishing locks.
Modules and Namespaces
- Code may be freely associated with a module by name by placing a module declaration statement in the source file; 3rd-party modules are easily extended in your code base without creating a fork of the original source.
- Data and functions are freely associated; no classes
- Modules and data structures have no visibility modifiers (no data hiding)
- Namespaces can be flattened with
useincluding withinstructdefinitions (nested data structure flattening) - Ability to piece-wise override functions from a module. E.g. change the behavior of a single function from a 3rd-party module without creating a local fork of the original source.
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: ", args.length);
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
== - Accessing array length with
.length - 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. These kinds of constructs are easy enough to create and use that Crescent
is not opinionated about them and only includes them as part of a standard library and does not introduce
try and option as intrinsic language types.
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).
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.
Some hold-overs from C syntax are kept. Some punctuation-based syntax in newer languages is rejected. For
instance, parentheses around flow-control conditions are kept. Some languages drop these in an aesthetic
attempt to make programming seem more natural-language-like, but this has the flaw that it decreases quick
scanability / parseability by humans. In the extent, if the entire program consisted of english words, its
scanability actually decreases. Some newer languages introduce a lot of punctuation-based syntax for their
differentiating features, ? as try type qualifier, :: delimiting type information, etc etc. In contrast to
the flow-control punctuation, Crescent attempts to limit punctuation syntax where there is already a delimiter
of some sort e.g. function parameters (first f32, second i32) is unambiguous and not improved by adding :
between parameter and type. The shift key is already used enough in programming.
struct StructName { }
enum EnumName type { }
union UnionName { }
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
- Explicit memory management. No garbage collection.
- Not removing punctuation-based syntax from flow-control conditions.
- Not adding punctuation-based syntax like
?and::
Sins to be Avoided
- Classes and other forms of object-oriented fixed encapsulation and data/code associations.
- Operator overloading. Yeah, I know, but the, like, seven really good
vec3fandmat4x4f-type use cases don’t cut it. Maaaybe binary operators excluding comparisons and assignment would be ok. Or maybe arrays-of-ints and arrays-of-floats get vector programming operators. - “Move” and “forward” semantics.
- UB soup and sequence point stew
More details
unit
any
bool
u8
u16
i16
u32
i32
u64
i64
f32
f64
strThese 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)
unitis a unit type (used likevoidin C). It has no size and contains no information.anyis a union that includes all defined types (both built-in and user-defined); it can be approximately thought of asvoid*in C except for two important differences: the corresponding type information is intact, it is a value and not a pointer.boolis the built-in boolean type; "true" and "false" are built-in keywords.stris 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]
fn | Define a function. |
struct | Define a data structure. |
enum | Define an enumerated type. |
union | Define a union type. |
const | Declare a constant (it must be initialized). |
var | Declare a variable (it may be initialized). |
type | Declare a new type (from an existing type). |
module | Declare the module the code in the source file is associated with. |
foreach
Simple array iteration (arrays are fixed-length).
var greeting_tokens str[] = ["Hello", " ", "world!", "\n"];
fn say_hello() unit {
foreach (token in greeting_tokens) {
// token is a string value,
// copied out of greeting_tokens
print(token);
}
foreach (token* in greeting_tokens) {
// 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 = greeting_tokens.length; 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 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
- Globals and functions defined in
_langare always in scope, and are referenced without their module prefix (_lang). And yes, this means you can add to the language this way (or global scope, if you will), but prudence is advised. - Files without a module declaration are implicitly associated with the
_appmodule. (Yourmain()function is fully qualified as_app.main)
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.
Crescent has no classes or polymophic inheritance type model. Crescent has no data hiding or encapsulation mechanisms. Essentially any OOP / OOAD ideas as they are widely known and have been taught are out. 👎️ More on that later.
Will likely add support for parameterized types for narrow versions of generics-programming.
Currently only experimental / unspecified. The JAI meta-programming model is excellent but would only be highly aspirational given the current language maturity level of Crescent.
Compiler
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 where the goal is only to explore combinations of language ideas and be feature-complete enough to compile a self-hosted compiler (crescent).
Mechanism
The compiler has a lot of standard stages like a resolution pass, a type-sizing pass, an inferrence pass (or more), a type-checking pass, a pass for checking basic errors like alloc-free mismatches / use-after-free, intermediate representation code generation.
Will like completely rip off the JAI compiler message loop idea. At least as it pertains to main stages so custom checkers and constraints can be added per code base.
Miscellany
To-Answer
“Why don’t you just use…?”
- Odin
- Zig
- C3
- Fil-C
- Verse / VerseLang
How is it different? Why is it different?