Flashbacks to TA'ing freshman programming 101 in Pascal: every student got hung up on when to use a period or semicolon or end. And from Austral's fib example (snipped)
module body Fib is
function fib(n: Nat64): Nat64 is
if n < 2 then
else
end if;
end;
end module body.
We here all understand BNF, Ada, Modula, etc and parsing but imagine explaining to the first day student:
Why is there no "end function" like for the other contexts? When do I use a semicolon vs a period to close a context? You shouldn't need the "railroad diagram" to understand the syntax.
I appreciate the explicit nest around module. I was just suggesting a symmetric and consistent syntax that can be stated simply, to align with the stated goal.
Dare I say, the (important) bit of Lisp syntax fits in a sentence! Yeah they hide the complexity in the library instead...
>> Which one of these corresponds to the second for loop? Unless we have an editor with folding support, we have to find the column where the second for loop begins, scroll down to the closing curly brace at that column position, and insert the code there. This is manual and error-prone.
I'm not fully convinced of that argument. Here's a devil's advocate take...
The keywords do indeed help when the corresponding nested control structures are off-screen, but if the code you are reading is not refactored to move the control structures into their own function so that the indentation doesn't get that much out of hand, you likely have bigger problems with the code than determining which `end` corresponds to which control structure.
IOW, having this sort of identification is moot: if it is needed, then the code itself is in such poor condition that it's likely not very readable anyway. In many cases it won't make a difference anyway (nested 'if's, for example - seeing multiple `end if` doesn't help) and the developer is still going to place a comment specifying which particular `if ()` is being ended.
Having the unadorned closing braces (`}`) leaves the developer one of three options:
1. Refactor that code just to be able to read it, or
2. As you point out, add in comments like `// end if`, etc, or
3. Leave it as it is.
If it's left as is, there's bigger problems in the code anyway.
This language looks super promising. With the exceptions of 'no type inference' and 'no arithmetic precedence', I really like its anti-features list.
With regard to 'no arithmetic precedence', I tried
printLn((1 + 2) + 3);
and
printLn(1 + 2 + 3);
Sure enough, the first one compiles, but the second doesn't.
Also, (n-1) is a parse error unless you put a space after the minus.
I got curious if recursion was properly handled, given it wasn't in the anti-features list, but no luck:
module body Foo is
function go(acc: Nat64, n: Nat64): Nat64 is
if n = 0 then
return acc;
else
return go(acc + n, n - 1);
end if;
end;
function main(): ExitCode is
printLn(go(0, 135000));
return ExitSuccess();
end;
end module body.
I prefer the rule in my own languages of "no arithmetic expressions whose meaning can be changed by adding parentheses". So `x + y - z` is allowed but `x - y + z` is not.
If operators are over-loadable, you support floats (in a non ffast-math mode), or you treat overflow in most non-modular-arithmetic ways, (x + y) - z and x + (y - z) are different.
Maybe it's worth saying "they're close enough to the same that parentheses should be optional", but I can definitely see the argument for just requiring them regardless.
I wouldn't rely on TCO being available, the bootstrapping compiler right now just emits very simple C (though GCC/LLVM might eliminate the recursion if they can).
Ideally I'd like stack overflow to be a clean abort rather than a stack overflow (just to make the error message more explicit) but I haven't got around to adding that.
That's curious - one of the example programs computes the fibonacci sequence. The language is clearly still a work in progress. I wonder what the difference is in your recursion and what's listed?
I disagree with you about 'no type inference'. I understand why some swear by type inference, but personally I prefer the complete clarity it provides to avoid it. If writing those characters annoys you, have tooling help you with avoiding that.
Austral’s module system is inspired by those of Ada, Modula-2, and Standard ML,
with the restriction that there are no generic modules (as in Ada or Modula-3)
or functors (as in Standard ML or OCaml), that is: all modules are first-order.
Modules are given explicit names and are not tied to any particular file system
structure. Modules are split in two textual parts (effectively two files), a
module interface and a module body, with strict separation between the two. The
declarations in the module interface file are accessible from without, and the
declarations in the module body file are private.
Crucially, a module A that depends on a module B can be typechecked when the
compiler only has access to the interface file of module B. That is: modules
can be typechecked against each other before being implemented. This allows
system interfaces to be designed up-front, and implemented in parallel.
It was a mistake how C++, Java and other languages forgot to split interface declaration from implementation definition, IMHO. Good to see that Austral learned from Modula-2.
Before I can form an opinion regarding Austral, though, I would need to see some larger programs implemented in it, for instance some low-level systems code, a generic data structure, some high-level business logic.
I guess there is no "strict separation" in C++, since that mechanism is, I believe, optional. Adding implementations to your header files might never pass PRs, but still.
Is no one going to talk about their capabilities system? That shit looks cool. A compile time permissions system for which resources can be used. I wonder how fool proof that can be made. Are there escape hatches in the form of arbitrary assembly/linking? Could a leftpad module security issue be deterred with this?
Yes, any leftpad-like security issue could be mitigated by the fact that you’d need to inject strange capabilities like network access to the leftpad function.
It is assumed this would raise eyebrows from the user of this function. Furthermore if you were to take a “safe” function and replace it with a dodgy one in a later version, the function signature would change and users would need to update their code. So nothing quite so brazen would get past.
Of course if you are mixing in arbitrary assembly/machine code in your binary via linking that might make a syscall and that could potentially be unsafe.
On July 3, 1940, as part of Operation Catapult, Royal Air Force pilots bombed the ships of the French Navy stationed off Mers-el-Kébir to prevent them falling into the hands of the Third Reich.
This is Austral’s approach to error handling: scuttle the ship without delay.
The British would probably be happy to do that even without the Third Reich. There are still British people today pissed about the French surrendering too quickly.
Given how controversial the British attack was on their allies even after the French assured them that ships would not be captured, I guess this passage is, as the kids say, "shots fired". :)
I really love the Design Goals and Rationale sections of the specification[1]. I have an interest in the landscape of new low-level languages like Odin[2], Vale[3] etc. Austral has the clearest "statement of intent" about how it is designed and where it is going.
On the contrary, I like it when the interface to my tools err on the side of precision at the cost of verbosity. I can always write a shell script or Makefile that does all the boring stuff once I've learned what the inputs mean, but if a tool only provides an overly simplified interface, it is much less obvious what it's doing, or what the other options might be, if any.
What does `go build` do? What files does it implicitly rely on? I have no idea. But I have a pretty good idea of what that `austral` command is going to do without having read any documentation about it.
You might, but I don't. In the argument `--entrypoint=Hello:main`, where does `Hello` come from? Is it some root module? What about `main`, is that some default, or the name of a file without an extension? This strikes me as just enough verbosity to be confusing, and not enough to be explicit.
The idea is the compiler has a bunch of explicit flags, but the build system (which doesn't exist yet) will have the `foo build`, `foo run` etc. commands and find the files using a package manifest.
Essentially like `cargo` vs. `rustc`. I have a little prototype of the build system in Python but haven't pushed it up yet.
what's encouraging you to conceive of the build system and the language as separate things? I never understood why most people making new languages seem to want to have each of these be distinct—why not just define the build using the same language?
I appreciated that it listed "no destructors" immediately after the top-line "no garbage collection", so I didn't need to read any further. What it means is it offers no ability to encapsulate resource management, so not useful for me. That doesn't mean it is not useful to others.
No, on the contrary, Austral is entirely built around resource management. The central concept is linear types, which is about ensuring 1) resources are disposed of and 2) resourced are used according to their protocol, e.g. no use-after-free.
There's "no garbage collection" because Austral lets you have manual memory management without the danger, like Rust.
There are no destructors in the sense of special destructor functions which are called implicity at the end of scope, or when the stack unwinds. Rather, you have to call the destructors yourself, explicitly, and if you forget the compiler will complain.
This sounds verbose until you start paying attention to all the mistakes you make all the time that involve, in some way, forgetting to use a value. The language makes it impossible to forget to do something.
People might feel like it is too verbose, but I think it is good to have the clarity. I write C at my day job and I have no problem with the 'verbosity' if it provides clarity of what happens. What I want is the compiler to help if I ever forget who owns a particular data value and miss to clean it up. For that linear types are perfect. I also prefer their simpleness over Rust's affine types which easily gets very complicated (see the difference between theirs and your borrow checker). Linear types gives me an easy way to define basic "state machines" for how to handle the data using types and then verifies that I implemented them correctly. That is kinda all I need. Feels like a good "get shit done" language.
From my reading of it, it does have what you're looking for. Specifically, while there are "no destructors", you are required to call a function to consume the value. Failing to consume the value is a compile time error. You can roughly approximate thinking about this as having destructors, but you're required to explicitly call them and the compiler won't let you write code that doesn't call the destructor.
This is nice. It does seem mutually exclusive with any early return though, like exceptions.
On the phone now so I only read the page on linear types, but will look at this closer when back at my desk.
In my own language I am considering destructors purely so that early returns are viable. I'd like to see if there is any alternative to destructors that aren't 'defer' or similar.
the page "What are linear types?" seems to address "resources" and the management thereof. not my cup of tea (but then, neither are destructors and garbage collection), but it's an interesting idea.
At least linear types means you'll never forget it. It also solves the problem of what to do when your destructor needs to error (e.g. closing a file).
The big downside is the verbosity of covering every branch of your code with your explicit close calls unless another mechanism is provided.
And it doesn't seem like succinctness is a top priority for this language.
Statements need an `end if`, `end for` etc. because it lets you find your way in nested code. The rationale for the syntax explains it a bit: https://austral-lang.org/spec/spec.html#rationale-syntax
FWIW I will probably get rid of the `module is ... end module.` bit because it adds unnecessary nesting.
Dare I say, the (important) bit of Lisp syntax fits in a sentence! Yeah they hide the complexity in the library instead...
From the link:
>> }
>> }
>> }
>> }
>> }
>> Which one of these corresponds to the second for loop? Unless we have an editor with folding support, we have to find the column where the second for loop begins, scroll down to the closing curly brace at that column position, and insert the code there. This is manual and error-prone.
I'm not fully convinced of that argument. Here's a devil's advocate take...
The keywords do indeed help when the corresponding nested control structures are off-screen, but if the code you are reading is not refactored to move the control structures into their own function so that the indentation doesn't get that much out of hand, you likely have bigger problems with the code than determining which `end` corresponds to which control structure.
IOW, having this sort of identification is moot: if it is needed, then the code itself is in such poor condition that it's likely not very readable anyway. In many cases it won't make a difference anyway (nested 'if's, for example - seeing multiple `end if` doesn't help) and the developer is still going to place a comment specifying which particular `if ()` is being ended.
Having the unadorned closing braces (`}`) leaves the developer one of three options:
1. Refactor that code just to be able to read it, or
2. As you point out, add in comments like `// end if`, etc, or
3. Leave it as it is.
If it's left as is, there's bigger problems in the code anyway.
Deleted Comment
With regard to 'no arithmetic precedence', I tried
and Sure enough, the first one compiles, but the second doesn't.Also, (n-1) is a parse error unless you put a space after the minus.
I got curious if recursion was properly handled, given it wasn't in the anti-features list, but no luck:
yieldsMaybe it's worth saying "they're close enough to the same that parentheses should be optional", but I can definitely see the argument for just requiring them regardless.
Ideally I'd like stack overflow to be a clean abort rather than a stack overflow (just to make the error message more explicit) but I haven't got around to adding that.
https://austral-lang.org/examples/fib
How deep you recurse :D
Before I can form an opinion regarding Austral, though, I would need to see some larger programs implemented in it, for instance some low-level systems code, a generic data structure, some high-level business logic.
Huh? C++ is split into header files (interface) and cpp files (implementation)...
It does enable header-only libraries though.
It is assumed this would raise eyebrows from the user of this function. Furthermore if you were to take a “safe” function and replace it with a dodgy one in a later version, the function signature would change and users would need to update their code. So nothing quite so brazen would get past.
Of course if you are mixing in arbitrary assembly/machine code in your binary via linking that might make a syscall and that could potentially be unsafe.
https://en.wikipedia.org/wiki/Attack_on_Mers-el-Kébir
[1]: https://austral-lang.org/spec/spec.html
[2]: https://odin-lang.org/
[3]: https://vale.dev/
`austral compile hello.aum --entrypoint=Hello:main --output=hello`
vs
`go build`
Etc etc.
What does `go build` do? What files does it implicitly rely on? I have no idea. But I have a pretty good idea of what that `austral` command is going to do without having read any documentation about it.
Essentially like `cargo` vs. `rustc`. I have a little prototype of the build system in Python but haven't pushed it up yet.
There's "no garbage collection" because Austral lets you have manual memory management without the danger, like Rust.
There are no destructors in the sense of special destructor functions which are called implicity at the end of scope, or when the stack unwinds. Rather, you have to call the destructors yourself, explicitly, and if you forget the compiler will complain.
This sounds verbose until you start paying attention to all the mistakes you make all the time that involve, in some way, forgetting to use a value. The language makes it impossible to forget to do something.
On the phone now so I only read the page on linear types, but will look at this closer when back at my desk.
In my own language I am considering destructors purely so that early returns are viable. I'd like to see if there is any alternative to destructors that aren't 'defer' or similar.
The big downside is the verbosity of covering every branch of your code with your explicit close calls unless another mechanism is provided.
And it doesn't seem like succinctness is a top priority for this language.
Dead Comment