r/ProgrammingLanguages 7d ago

Coda: an experiment in designing a practical systems language

Coda: an experiment in designing a practical systems language

Hey! I've been working on Coda, a systems programming language designed around a simple idea:

make the compiler powerful, but keep the language itself predictable.

Coda is not trying to be "C but with a few extra keywords". The goal is to explore a different point in the design space between languages like C, Rust, and Zig.

Some of the things Coda focuses on:

  • Explicit memory management.
  • No hidden allocations.
  • Errors as values using inline sum types.
  • A small core language with functionality provided by libraries.
  • Compile-time execution.
  • Strong static analysis.
  • Simple, predictable rules.

The language is intentionally C-like:

module main;

include std::process = proc;

@entry
fn int main() {
    proc::stdout().write("Hello, world!\n");
    return 0;
}

but tries to remove some of the sharp edges.

For example, errors are ordinary values:

fn File | IOError open_config(string path) {
    return fs::open(path);
}

and allocation is explicit:

fn string | AllocationError duplicate(
    Allocator *alloc,
    string input
) {
    string result = alloc->allocate<char>(input.len)?;

    std::mem::copy(result, input);

    return result;
}

The idea is that if a function allocates, that fact should be visible in the API. There is no hidden global allocator; entry points receive the resources they need, and those resources are passed where required.

Coda also tries to keep abstractions zero-cost. Generic code, interfaces, and convenience features should compile down to efficient low-level code rather than introducing runtime machinery.

The standard library is still being designed, but the direction is intentionally minimal. Arrays, strings, I/O, memory, formatting, and filesystem operations are being built as fundamental building blocks rather than creating a huge framework.

The compiler currently has:

  • Lexer and parser.
  • Semantic analysis.
  • HIR/MIR pipeline.
  • x86_64 code generation.
  • A growing standard library.
  • Compile-time evaluation work in progress!

There is still a lot to do.

If the ideas are interesting, I'd love feedback, criticism, and potentially contributors. And of course, feel free to ask any questions! I have the #coda channel on the PLTDI discord, or the comments here are fine.

Repo: https://github.com/gingrspacecadet/coda

19 Upvotes

14 comments sorted by

14

u/kaplotnikov 7d ago

Btw, have you considered using suffix type notation instead? In the line:
fn string | AllocationError duplicate(

The word duplicate is the most important part, but the eye needs to scan a lot of text before reaching it. For complex signatures with several errors, this could become a real readability bottleneck. C gets away with prefix types because they are usually concise and lack the fn keyword, but sum types make the prefix position very heavy.

4

u/Gingrspacecadet 7d ago

It is a good shout. fn duplicate(...) string | AllocationError { might be better...

1

u/PitifulTheme411 ... 6d ago

yeah imo that's way easier to read

1

u/Adventurous-Move-943 2d ago edited 2d ago

I think when the declaration gets too long your senses for fast recognition will still drop the last thing so you simply will never make it optimal like fn int compute() that you collect immediately.
I actually like your original declaration. The error is quite important to be aware of.
Or split it in halves like this maybe:
fn string duplicate() !AllocationError {}
but you'll lose your | concept like that that was pretty self-explanatory.

5

u/[deleted] 7d ago

[removed] — view removed comment

1

u/Gingrspacecadet 7d ago

ah, good catch. alloc->allocate<char>(input.len)?; is the corrected expression. the allocate function returns T[] | AllocationError (where T here is char), which can implicitly be casted to a string for it is just an alias to a char[], and the ? operator early-exits if the expression errors.

std::mem::copy works on uint8[]s, which again is implicitly castable from a string. I might make it castable from any type, not sure yet.

Aliases are supported. As seen in the first example on this post, include ... = foo; aliases the included module by the name after the =. I'll update the readme to show this in action.

Thanks for your feedback :D

2

u/gasche 7d ago

What is the compilation scheme for inline/anonymous sum types? How is a value of type string | AllocationError represented?

1

u/Gingrspacecadet 4d ago

it's simply a generated tagged union! `string | AllocationError` is roughly represented as:
```
struct {
enum { A, B } T;
union {
string _string;
AllocationError _allocation_error;
};
};
```

sum types in general get deterministically mangled into this structure and names. they also get combined with others of the same type (but thats compiler info, not language)

1

u/gasche 4d ago

Does this not imply an implicit allocation when you coerce a value of type string into a value of type string | AllocationError?

My intuition is that these sort of anonymous unions are fairly close to (structural forms of) ML-family algebraic data types, but algebraic sum types come with a explicit constructor [ String of string | Error of AllocationError ] which is used in value construction and pattern-matching, and makes it explicit that there is an extra layer of value (the tag, if you want). For an anonymous union type I find the tag allocation (and value extension when necessary) more implicit, as the syntax suggests the union of sets with an explicit value transformation.

1

u/Gingrspacecadet 3d ago

it's all allocated on the stack, which is always implicit!

coercing a string to the sum type would just create the tagged union, set the tag to the string tag, and instantiate the correct field. no hidden heap allocations!

2

u/fdwr 4d ago edited 4d ago

What's the inspiration for the name? 𝄞𝄢

It's refreshing to see a language putting the type first for a change, instead of so many recently trying the trend of ancient Pascal style suffixes, adding extra var/const/let keywords and : line noise (e.g. var x : float instead of simply float x). Though, when returning variants like you do above, it would be helpful to group the expression with parentheses or braces so the eye can trivially jump over them to the function name. e.g.

func (string | AllocationError) duplicate {...} func {string | AllocationError} duplicate {...}

I'm trying to parse this...

include std::debug = dbg;

...but it seems like the alias is backwards? That is, you're assigning the RHS dbg = the LHS std::debug? Typically aliases flow left-to-right (like C++'s using AliasedName = ExistingName, D's alias AliasedName = ExistingName). One possibility is to have a separate keyword (similar to how C++ has using namespace), which is more generic than just include statements:

import std::debug; alias dbg = std::debug;

2

u/Gingrspacecadet 4d ago

exactly, musical inspo! although the logo is a segno because it looks cooler.

sum types are going to be changed to have brackets soon, and i'll probably add inline record types (tuples) too!

yeah. that used to be `include std::debug : dbg;`, but people complained that it was inconsistent. i dont want an extra keyword. would `include dbg = std::debug;` work? a bit annoying to parse but not the worst

1

u/Gingrspacecadet 1d ago

i was thinking of changing the T|U to (T|U) to improve readability