Important
About 2 weeks ago (as of 01/31/2018), I started having a huge number of breakthroughs that stemmed from a complete shift in my ideology and how I approach language design. This has completely changed the language 100% for the better, and everything mentioned before this post is thus
invalidated.
Design Guidelines (Summary)
- Don't make coders repeat themselves.
- Don't try to prevent mistakes.
- "Correct" means "deterministic".
Design Guidelines (Details)
- I assert that the primary factor for deciding on the syntax of a language and adding new features is that coders shouldn't have to repeat themselves. Repetition is noise, which makes code difficult to read, write, and change.
- I assert that trying to prevent coders from making mistakes is a bad thing. Preventative measures are always overly obtuse, inefficient, and difficult to comprehend and execute in practice. The reduction of errors should come naturally as a side-effect of the simplicity of the language and it's ability to express the algorithm which is being written.
- I assert that mathematical correctness is not true correctness in the field of programming; rather, true correctness is determinism based on well-defined behavior. In the context of unsigned 8-bit integers, "2 + 255 = 1" is a correct expression because it's defined to be so.
Modules
Adhering to the first design guideline (the DRY principal), I wanted to allow the ability to import multiple modules at the same time, and I also didn't want the symbols from libraries to automatically pollute the current scope. So I decided that I would borrow the use of `.*` from Java to explicitly indicate this.
I also decided that I wanted a way of bundling modules together, which didn't depend on namespace nesting like in C++
Cleaning Up Resources
A very common issue, and one of the remaining 3 justifications for the existence of goto, is cleaning up resources when you have multiple points of failure. For that, I decided to borrow the `defer` statement from Go, and a defer would push the statement onto a stack so that they're executed in the opposite order of which they're deferred. So in the below code:
Code: Select all
FILE *f1, *f2;
f1 = fopen("file1", "rb");
if (!f1)
return;
defer fclose(f1);
f2 = fopen("file2", "rb");
if (!f2)
return;
defer fclose(f2);
return;
the final return statement will execute `fclose(f2)` before `fclose(f1)`.
Unconditional Loops
A very common idiom is the use of an unconditional loop. Not just the `while(true)` or `for(;;)`, but C has a `do/while` statement that basically creates an unconditional loop with an `if (condition) break;` statement at the end of it. I intended to fix this, and eliminate the second justification for goto by adding a `recur` statement:
Code: Select all
recur {
getEvents();
if (quit);
break;
update();
render();
}
Labelled Loop
To kill the 3rd and final justification for goto, which is to break/continue in nested loops, I decided to keep labels for loops and switch statements.
Bounded Arrays
I wanted to have bounded arrays, but I just couldn't decide on what the maximum size of a bounded array should be. Turns out, it's
very dependent on what you're doing. So then I thought, why not do this:
No allocation is being performed, we're just saying that `val` is a pointer which is bounded by `len`; and unlike the fixed-length arrays in C, `val`can be redirected.
Use All The Things
Two particular things I wanted to deal with were the ability to use the symbols of a module in a specific scope, and allow for compositional polymorphism of structures by flattening the namespaces. For this, I added a `use` keyword:
Code: Select all
struct vec2 { float x, y; };
struct vec3 {
use vec2;
float z;
};
vec3 v;
v.x = 5;
As I thought about this idea more, I began to wonder, what if you could `use` an anonymous procedure to overload the [] and () operators of the struct?
Code: Select all
struct Table {
int val;
use proc(Table *this, char *key) int * {
return &this.val;
}
}
Table table;
table["test"] = 6;
struct Reader {
FILE *f;
use proc(Reader *this, int len, void *buf) int {
return fread(buf, 1, len, this.f);
}
}
Reader read;
int i;
read.f = fopen("test.txt", "rb");
defer fclose(read.f);
read(sizeof(i), &i);
This is an extremely powerful abstraction that afaik, has never been used before. But we're only getting started!
Like Closures...But Waaaaayyy Better
Before I came up with the idea for using procs in structs, I was thinking hard about how you could bind data to a procedure, and came up with this idea:
Code: Select all
proc Read(Reader *reader, int len, void *data) int;
Reader reader;
int i;
var read = Read(reader, ..);
read(sizeof(i), &i);
What's happening under the hood is this:
Code: Select all
var read = proc(int len, void *data) int { return read(reader, len, data); }
In other words, the `..` causes all the previous arguments to be captured, and a new anonymous function is created which closes over the function being called and the captured values. This also means that you can do this:
Code: Select all
Read(reader, ..)(sizeof(int), ..)(&i);
Turns out, this can be used to create iterators, coroutines, generators, and trampolines. I eventually found out there was a technical name for it called
currying. I'm currently exploring the idea of using the `=>` operator together with currying as a way of creating pipelines.
Despite the fact that we can easily create trampolines, I do want to mention that tail-call recursion will still be a feature.
Stateful Machines
Another one of the problems on my todo list was to fix the broken switch statements in C. First (and least important) is the name. `switch` and `case` are very common words. I decided to replace them with `when`, `is`, and `or`. The `or` keyword doesn't mean `logical or` btw. It's a fallthrough case; because the `when` statement
breaks by default.
But I wasn't necessarily done with switch statements yet. I wanted some way to be able to build state machines out of `when` statements. After much pondering, the idea hit me of having the value you're checking being like a tape. You read a value and either reach a breaking state or you `shift` to the next state:
Code: Select all
when ("hello") {
is 'h':
or 'l':
print("consonant");
shift;
is 'e':
or 'o':
print("vowel");
shift;
}
With the newly changed semantics, I decided to replace the `continue` keyword with `shift`.
EDIT:
I forgot to mention, procedures are const by default, which was the original reason for having a `var` keyword. To create a function pointer, you'd use `var proc`. Both `const` and `var` can be used as either a modifier or a way to declare an identifier with type inference.
Code: Select all
var proc glBindBuffer(uint target, uint buf);
...
glBindBuffer = dlsym(dl, "symbol");
const pi = 3.1415926535;
var text = "stuff";
EDIT #2:
A proposal for the order of operations:
Code: Select all
• Unary Suffix Operators () [] . ++ --
• Unary Prefix Operators & * ! ~ ++ -- + -
• Multiplication *
• Division & Remainder / %
• Addition & Subtraction + -
• Bitmasking Operators & | ^
• Bitshifting Operators << >>
• Relational Operators == < > != <= >=
• Logical Operators && || ^^
• Ternary Conditions ?:
• Assignment Operators = += -= *= /= %= <<= >>= &= |= ^=
There are a lot of differences between this and the C order operations. For example:
Code: Select all
1 ^ 4 << 1 == 10 // in C, the right hand side would be `9`
x & y == 7 // in C, this would be evaluated `x & (y == 7)`
4 / 2 * 2 == 1 // in C, this would be evaluated `(4 / 2) * 2 = 4`
false || true && false // in C, this would be evaluated `false || (true && false)`