The 2024 Wheel Reinvention Jam just concluded. See the results.

The Odin Programming Language

For the past year I've been trying to fix C with my metaprogramming tools and I've came to the conclusion that C is "unfixable". C's syntax and type system are both fundamentally broken and cannot be "fixed" without it looking like a different language. There are some things that cannot be expressed in C, even with "fixing" it. I've done all the "improvements" you've suggested and much, much more and that doesn't solve the underlying problems with the language. There is a need for a new language but fixing C doesn't solve it. I want a better sanity of life with a new language and fixing C isn't that.

Also, how is Cling what I want? "Cling is an interactive C++ interpreter..." and "...uses just-in-time (JIT) compiler for compilation." This seems like the exact opposite to what I want.

The language dale, doesn't appeal to me. It does have some nice "features" but not what I need to aid me solve my problems. My language has a lot of the "features" already and plus, it's not in the lisp-syntax.
gingerBill

Also, how is Cling what I want? "Cling is an interactive C++ interpreter..." and "...uses just-in-time (JIT) compiler for compilation." This seems like the exact opposite to what I want.


"Ability to run any code at compile time"
* Being able to run interactively/JITed allows you to run arbitrary compile time code.

"Unified syntax between main language and metaprogramming language"
* Likewise, being able to to do JIT means you can do whatever code generation and other metaprogramming you need to in C or C++.

"Built-in introspection for all types"
* Might not be mentioned on the front-page but IIRC all the metadata about all the compiled code and complete AST is also exposed through the library giving you access full introspection as well as the complete capabilities of the compiler.

"No need for external tools such as Make/IDE"
* Again, JIT + it's simple commands for linking, including, etc. allows you to define your complete build process in C.

"Simple to read and write", "Easy to comprehend and reason with", "Fast", "Low-level", "Compiled, strongly-typed, static language", "Higher control of data layout and data access"
* Sounds like C to me?

If none of that appeals to you then fair enough, "your usage may vary" as they say =) but that was my reasoning anyway.

Edit: perhaps I should also stress that Cling isn't _only_ JIT - it still has the full feature set of LLVM+Clang.

Edited by Michael Cameron on
Idea for Odin:

Possible names: "data label" or "struct label" or "member label".

The idea is that, right now it is pretty inconvenient if you have a pointer that you know points to a particular member of a struct, and you want to retrieve the pointer to the entire struct. For instance suppose I have an entity type and my entities each have two "Node" structs because they can all be members of up to do two different doubly linked lists in my system:

1
2
3
4
5
6
struct Entity{
    Node node1;
    Node node2;
    float x,y;
    // other members
};


Rather that write them as externally stored lists, I have put the next/prev pointer right into the structs via the Node members. This means I just have to write one doubly linked list system that works on the Node type. Now the only issue is that when I'm in one of the lists that links through node2, I should in theory be able to use those nodes to get back to the entities. In this case you would subtract one from the node pointer then cast, but I occasionally find myself in nastier cases. With the offsetof operator this get's a little nicer because now we can write code that figures out the right pointer adjustment as long as we know the name of the member we're offsetting from. But then something struck me...

Why should I have to cast it back to an entity at all? Once I have I'm going to access members of the entity which means the compiler will be adding some other fixed amount back to the pointer. In simple cases that might be optimized out, but the nicer thing would be if the compiler just knew that what I wanted was a pointer to an Entity struct, but not based at the beginning of the struct, but rather at node2. With that info baked into the pointer type, it could then automatically use the correct additional offset to find each member.

So maybe in Odin there would be pointers like:
1
2
3
    entity : ^Entity.node2 = node_ptr;
    entity->x += dx;
    entity->y += dy;


Now entity is a pointer to an Entity struct, but it is based at node2 instead of the beginning of the struct.

Another way this might be done is by literally adding labels:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
Entity :: type struct {
    #label kill_list
    node1: Node;
    #label need_processing_list
    node2: Node;
    x,y: float;
}
    //...
    entity : ^Entity.need_processing_list = node_ptr;
    entity->x += dx;
    entity->y += dy;
    //...


There are a lot of other details about this that could be expanded upon, but I just wanted to share the idea. I've got no idea if it's any good or not, but I think this has hit me a few times in recent memory.

Edited by Allen Webster on Reason: grammar
Weird that you ask that Allen, as I was just thinking the exact thing before reading this post. I've been making "everything is a namespace" at the moment which is very similar to Jai's using or Pascal's with. As a consequence this brings up questions about subtyping.

The first that I thought of going from a subtype to the "base type".

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
Entity :: type struct {
    using pos: Vec2
    name: string
}

Frog :: type struct {
    using entity: Entity
    jump_height: f32
}

f: Frog
f.x = 1 // access subtype's used fields and dereferences when needed
using f
x = 1 // f's members are in this namespace now

thing :: proc(e: Entity) { ... }
thing(f.entity)
thing(f) // Maybe this should work


If this works, then I should be able to pass the Frog type to any procedure that takes an Entity type because it subtypes Entity. So why not make it work in reverse?

But the problem then comes down to two things, semantics and syntax. When does it work and how do you define when it is allowed? I'll have to think it through fully before implementing.

I do want to implement tagged/discriminated unions too into the language which should help with a lot of the same problems but with a different memory layout (which is much better than this subtyping method).


P.S. You'll notice, semicolons are now optional. It just looks weird to me mainly because I'm not used to it :P

Edited by Ginger Bill on
Odin is looking pretty cool! Nice job, Bill.

- Connor
gingerBill

P.S. You'll notice, semicolons are now optional. It just looks weird to me mainly because I'm not used to it :P


Ahaha, I really like semicolons. Even way back when I was using lua which didn't require them I eventually decided to always use semicolons as a matter of style.
Mr4thDimention
gingerBill

P.S. You'll notice, semicolons are now optional. It just looks weird to me mainly because I'm not used to it :P


Ahaha, I really like semicolons. Even way back when I was using lua which didn't require them I eventually decided to always use semicolons as a matter of style.


After about two days of javascript coding without them, plus the Go stuff I've done in the past, I don't miss them at all.
gingerBill
Weird that you ask that Allen, as I was just thinking the exact thing before reading this post. I've been making "everything is a namespace" at the moment which is very similar to Jai's using or Pascal's with. As a consequence this brings up questions about subtyping.

The first that I thought of going from a subtype to the "base type".

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
Entity :: type struct {
    using pos: Vec2
    name: string
}

Frog :: type struct {
    using entity: Entity
    jump_height: f32
}

f: Frog
f.x = 1 // access subtype's used fields and dereferences when needed
using f
x = 1 // f's members are in this namespace now

thing :: proc(e: Entity) { ... }
thing(f.entity)
thing(f) // Maybe this should work


If this works, then I should be able to pass the Frog type to any procedure that takes an Entity type because it subtypes Entity. So why not make it work in reverse?

But the problem then comes down to two things, semantics and syntax. When does it work and how do you define when it is allowed? I'll have to think it through fully before implementing.

I do want to implement tagged/discriminated unions too into the language which should help with a lot of the same problems but with a different memory layout (which is much better than this subtyping method).


P.S. You'll notice, semicolons are now optional. It just looks weird to me mainly because I'm not used to it :P


If tagged unions work better than subtyping, why introduce it at all?
I don't know about Go, but I know that there are some situations in JS that are ambiguous and require semicolons if you want the code to be interpreted correctly. I would definitely recommend using them in JS. Also, I think semicolons are actually quite important if you want your language to support multi-line statements (which it should). You can work around this in JS by carefully choosing where to split your lines, but it's kind of a mental burden and prone to mistakes.
CaptainKraft
If tagged unions work better than subtyping, why introduce it at all?

Because the concepts are nearly orthogonal. You can use both to achieve other things that both couldn't do alone.

ClysmiC
I don't know about Go, but I know that there are some situations in JS that are ambiguous and require semicolons if you want the code to be interpreted correctly. I would definitely recommend using them in JS. Also, I think semicolons are actually quite important if you want your language to support multi-line statements (which it should). You can work around this in JS by carefully choosing where to split your lines, but it's kind of a mental burden and prone to mistakes.


I don't insert semicolons on every line like JavaScript. I have a very simple set of rules for when semicolons are needed. If I expect a semicolon after the end of a statement/declaration I check this:

1
2
3
if      - the next token is not on the same line, no need for a semicolon
else if - the next next is a close brace, no need for a semicolon
else    - semicolon is needed (multiple statements on a single line)


Semicolons are needed as separators within for loops, if inits, etc.

Making semicolons optional actually removes some ambiguities in the syntax.
gingerBill
Valmar:
timothy.wright:
I think Go got a lot of thinks correct but a lot of things wrong for it to replace C/C++. Mainly garbage collection and ability to do "unsafe" things is restricted and limiting. Having written hundreds of thousands of lines of Go already, I can say for definite, I cannot replace C/C++ (in their domains).

Go gets one thing absolutely right - it compiles fast. It was designed to compile fast.

I got all excited as I was reading about it - finally, finally somebody gets IT.
Compile times are like the most important thing ever.

Then I discovered that Go code is statically linked and doesn't allow to create dynamically linked libraries.
What's the point of fast compile times if you can't hotswap code at runtime? Deal breaker right here.

Nobody gets it. Jon save us.
My language already supports dynamic linking as I leverage LLVM as the backend. I understand the need as I need it too. For Go's problem domain, dynamic linking isn't usually needed. However, it is still possible in Go, just not very easy. One of Go's reasonings for statically linking everything is that you reduce dependencies. If you have an .exe compiled from Go, you don't have to worry about installing DLLs needed to run the program.

Hotswapping is a very useful feature but I'm not sure if it should be a language level feature or a library level feature yet.

--

I should note, when I'm talking about "Go getting a lot right", I'm not just talking about fast compilation speeds. I'm talking about how simple, elegant, and pragmatic the language is but still expressive. I can keep the entirety of the language within my head which increases my sanity of life (which cannot be said for C++).

I'm making a "better C" (for a better phrase), not a "better C++".

At the moment, my language does quite of lot of what Jon's language does already but not everything, yet. I'm lacking polymorphic records and procedures, arbitrary compile time execution, procedure and operator overloading, and default procedure parameters.

I'm not even sure if I want to add overloading to the language as it adds a bit of complexity to the type system and namespacing. I'd say 99% of my "needs" for overloading come from mathematical types and operations. Having vectors as first class type (the mathematical kind not the silly C++ construct), infix/postfix calls, and nested procedures in the language aids a lot of my problems are already.
gingerBill


I'm not even sure if I want to add overloading to the language as it adds a bit of complexity to the type system and namespacing. I'd say 99% of my "needs" for overloading come from mathematical types and operations. Having vectors as first class type (the mathematical kind not the silly C++ construct), infix/postfix calls, and nested procedures in the language aids a lot of my problems are already.


For overloading maybe only let marked functions be overloadable.

That way you could create something like:

1
2
3
4
abs :: overload{ (f : float ) -> float{return ...}, 
                 (f : int ) -> int{return ...}, 
                 ...
               }


"abs" is then a special construct that represents all the functions which will implicitly convert to a function of the correct signature as needed. Then you also have the option of getting a specific one by doing "fabs := abs as (float)->float". This would also fix one of my general complaints with overloading, which is that it's non-obvious how to reference one specific variant of the overloaded function.
gingerBill
I should note, when I'm talking about "Go getting a lot right", I'm not just talking about fast compilation speeds. I'm talking about how simple, elegant, and pragmatic the language is but still expressive. I can keep the entirety of the language within my head which increases my sanity of life (which cannot be said for C++).


Very interesting that you see all those qualities in Go: most people say its a horrible designed language, contrasting it to Rust.

But I'm with you: its nice to use, get-things-done approach, deploys easily: a better alternative to C to ship tools that work on the 3 platforms.

(I do miss Python's expressivity when writing Go, specially for writing web apps and such)

Edited by hugo on
ratchetfreak --

That's not a bad idea. That is the kind of "generics" that work in C11. Also, that would not completely ruin the type system and namespacing too much as it's explicit. And I could implement it quite easily too. I'll have to try it this evening and see what it's like.

1
2
3
#define cbrt(x) _Generic((x), long double: cbrtl, \
                              default: cbrt, \
                              float: cbrtf)(x)



hugo --

That's my problem with rust -- it's "too complex" in comparison. However, rust is still a language to keep an eye on as it is seeming to gain a little momentum.